li.shaoyi 3 лет назад
Родитель
Сommit
c3d47f5cb6
27 измененных файлов с 2080 добавлено и 210 удалено
  1. 2 2
      src/common/components/echart/echart-timeline/index.vue
  2. 0 14
      src/common/components/echarts/echart-base/index.ts
  3. 0 55
      src/common/components/echarts/echart-base/index.vue
  4. 56 0
      src/common/components/echarts/echarts-base/index.less
  5. 59 0
      src/common/components/echarts/echarts-base/index.vue
  6. 103 0
      src/common/components/echarts/echarts-base/setup.ts
  7. 252 0
      src/common/components/echarts/echarts-kline/dataset.ts
  8. 57 0
      src/common/components/echarts/echarts-kline/index.less
  9. 316 0
      src/common/components/echarts/echarts-kline/index.vue
  10. 353 0
      src/common/components/echarts/echarts-kline/options.ts
  11. 93 0
      src/common/components/echarts/echarts-kline/type.ts
  12. 128 0
      src/common/components/echarts/echarts-timeline/dataset.ts
  13. 19 0
      src/common/components/echarts/echarts-timeline/index.less
  14. 137 0
      src/common/components/echarts/echarts-timeline/index.vue
  15. 239 0
      src/common/components/echarts/echarts-timeline/options.ts
  16. 38 0
      src/common/components/echarts/echarts-timeline/type.ts
  17. 30 0
      src/common/components/tabbar/index.less
  18. 48 0
      src/common/components/tabbar/index.vue
  19. 7 0
      src/common/components/tabbar/type.ts
  20. 29 27
      src/hooks/account/index.ts
  21. 2 0
      src/services/go/ermcp/futures/interface.ts
  22. 2 2
      src/views/market/futures/compoments/futures-trade/index.vue
  23. 3 1
      src/views/market/spot_trade/components/goods-chart/chart/index.less
  24. 101 107
      src/views/market/spot_trade/components/goods-chart/chart/index.vue
  25. 3 1
      src/views/order/futures_information/components/futures_information_entrust/columns.tsx
  26. 3 1
      src/views/order/futures_information/components/futures_information_success/columns.tsx
  27. 0 0
      src/views/report/future_report/list/future_report/index.less

+ 2 - 2
src/common/components/echart/echart-timeline/index.vue

@@ -95,8 +95,8 @@ export default defineComponent({
       }
 
       return {
-        max: max + max * 0.01,
-        min: min - min * 0.01,
+        max: max,
+        min: min,
       };
     };
 

+ 0 - 14
src/common/components/echarts/echart-base/index.ts

@@ -1,14 +0,0 @@
-// 创建 iframe 对象
-const createIframe = (parent: HTMLElement): HTMLIFrameElement => {
-    const iframe = document.createElement('iframe');
-    // 设置 iframe 样式
-    iframe.style.cssText = 'position: absolute; z-index: -1000; left: 0; top: 0; width: 100%; height: 100%; border: 0; visibility: hidden; pointer-events: none;';
-    // 添加 iframe 节点
-    parent.appendChild(iframe);
-
-    return iframe;
-}
-
-export function useEcharts(el: HTMLElement) {
-
-}

+ 0 - 55
src/common/components/echarts/echart-base/index.vue

@@ -1,55 +0,0 @@
-<template>
-  <div ref="chartElement" class="mtp-echarts" :style="{ width: width, height: height }">
-    <template v-if="empty">
-      <div class="mtp-echarts__empty">暂无数据</div>
-    </template>
-    <template v-else>
-      <div class="mtp-echarts__container" v-for="i in options.length" :key="i"></div>
-    </template>
-  </div>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref, onMounted, PropType, watch, watchEffect } from 'vue';
-import { EChartsOption } from 'echarts';
-
-export default defineComponent({
-  props: {
-    // 图表宽度
-    width: {
-      type: String,
-      default: '100%',
-    },
-    // 图表高度
-    height: {
-      type: String,
-      default: '100%',
-    },
-    // 图表配置项,支持多图表(待完善...)
-    options: {
-      type: Array as PropType<EChartsOption[]>,
-      default: [],
-    },
-    loading: {
-      type: Boolean,
-      default: true,
-    },
-    empty: {
-      type: Boolean,
-      default: false,
-    },
-  },
-  setup(props) {
-    // 图表容器元素
-    const chartElement = ref<HTMLElement>();
-
-    return {
-      chartElement,
-    }
-  }
-})
-</script>
-
-<style lang="less">
-@import './index.less';
-</style>

+ 56 - 0
src/common/components/echarts/echarts-base/index.less

@@ -0,0 +1,56 @@
+.mtp-echarts {
+    flex: 1;
+
+    &.is-empty {
+        position: relative;
+
+        &::before {
+            content        : '暂无数据';
+            position       : absolute;
+            top            : 0;
+            left           : 0;
+            z-index        : 1;
+            display        : flex;
+            justify-content: center;
+            align-items    : center;
+            width          : 100%;
+            height         : 100%;
+        }
+
+        >div {
+            z-index: -100;
+        }
+    }
+
+    .tooltip {
+        &-title {
+            text-align: left;
+        }
+
+        &-item {
+            display    : flex;
+            align-items: center;
+
+            span:first-child {
+                display       : table-cell;
+                width         : 40px;
+                text-align    : left;
+                vertical-align: middle;
+            }
+
+            span:last-child {
+                width     : 100px;
+                text-align: right;
+            }
+
+            i {
+                display      : inline-block;
+                font-size    : 0;
+                margin-right : 8px;
+                border-radius: 50%;
+                width        : 7px;
+                height       : 7px;
+            }
+        }
+    }
+}

+ 59 - 0
src/common/components/echarts/echarts-base/index.vue

@@ -0,0 +1,59 @@
+<template>
+  <div ref="chartElement" :class="['mtp-echarts', empty && 'is-empty']" :style="{ width: width, height: height }"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType, watch } from 'vue';
+import { EChartsOption } from 'echarts';
+import { useEcharts } from './setup';
+
+export default defineComponent({
+  props: {
+    // 图表宽度
+    width: {
+      type: String,
+      default: '100%',
+    },
+    // 图表高度
+    height: {
+      type: String,
+      default: '100%',
+    },
+    // 图表配置项
+    option: {
+      type: Object as PropType<EChartsOption>,
+      default: () => ({}),
+    },
+    index: {
+      type: Number,
+      default: 0,
+    },
+    loading: {
+      type: Boolean,
+      default: false,
+    },
+    empty: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  setup(props) {
+    const { chartElement, setOptions, showLoading } = useEcharts();
+
+    watch(() => props.loading, (val) => showLoading(val));
+    watch(() => props.option, (val) => {
+      if (!props.empty) {
+        setOptions(val)
+      }
+    });
+
+    return {
+      chartElement,
+    }
+  }
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 103 - 0
src/common/components/echarts/echarts-base/setup.ts

@@ -0,0 +1,103 @@
+import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue'
+import { debounce } from '@/utils/time'
+import { v4 } from 'uuid'
+import { getTheme, ThemeEnum } from '@/common/config/theme';
+import ResizeObserver from 'resize-observer-polyfill';
+import * as echarts from 'echarts'
+
+// 默认配置项
+const defaultOption: echarts.EChartsOption = {
+    backgroundColor: 'transparent',
+    animation: false,
+    grid: {
+        top: 20,
+        bottom: 40,
+    },
+    axisPointer: {
+        show: true,
+        triggerTooltip: false,
+    },
+}
+
+export function useEcharts() {
+    const context = getCurrentInstance();
+    const chartElement = ref<HTMLElement>(); // chart 元素
+    let chart: echarts.ECharts; // chart 对象
+
+    // 图表配置项
+    const setOptions = (option: echarts.EChartsOption, notMerge = false) => {
+        chart.setOption(option, notMerge);
+        chart.hideLoading();
+        context?.emit('update:loading', false);
+    };
+
+    // 显示加载动画
+    const showLoading = (loading: boolean) => {
+        if (loading) {
+            chart.clear(); // 清空图表
+            chart.setOption(defaultOption);
+
+            const theme = getTheme(); // 当前主题
+            switch (theme.value) {
+                case ThemeEnum.default:
+                case ThemeEnum.dark:
+                    chart.showLoading({
+                        text: '加载中...',
+                        textColor: '#fff',
+                        color: 'rgba(255, 255, 255, 0.75)',
+                        maskColor: 'transparent',
+                    });
+                    break;
+                default:
+                    chart.showLoading({
+                        text: '加载中...',
+                        maskColor: 'transparent',
+                    });
+            }
+        }
+    };
+
+    // 图表初始化
+    onMounted(() => {
+        const uuid = v4();
+        const el = chartElement.value;
+
+        if (el) {
+            // 初始化图表
+            chart = echarts.init(el as HTMLElement);
+            chart.setOption(defaultOption);
+            chart.off('mousemove');
+
+            // 监听鼠标滑动
+            chart.getZr().on('mousemove', (params) => {
+                const pointInPixel = [params.offsetX, params.offsetY];
+                if (chart.containPixel('grid', pointInPixel)) {
+                    const pointInGrid = chart.convertFromPixel({ seriesIndex: 0 }, pointInPixel)
+                    const dataIndex = pointInGrid[0];
+                    if (dataIndex > -1) {
+                        context?.emit('update:index', dataIndex);
+                    }
+                }
+            })
+
+            // 监听元素变化
+            new ResizeObserver(() => {
+                debounce(() => {
+                    chart.resize && chart.resize(); // 重置图表大小
+                }, 50, uuid);
+            }).observe(el);
+
+            context?.emit('ready', chart);
+        }
+    })
+
+    onUnmounted(() => {
+        chart?.dispose();
+    })
+
+    return {
+        chartElement,
+        showLoading,
+        setOptions
+    }
+}

+ 252 - 0
src/common/components/echarts/echarts-kline/dataset.ts

@@ -0,0 +1,252 @@
+import { QueryHistoryDatasRsp } from '@/services/go/quote/interface';
+import { Dataset, Candlestick, MACD, VOL, KDJ, CCI } from './type'
+import moment from 'moment';
+
+export function useDataset() {
+    // 行情历史数据中所有补充数据的索引位置(用于指标计算)
+    const invalidData: number[] = [];
+
+    // K线数据
+    const klineData: Dataset<Candlestick> = {
+        dimensions: ['date', 'open', 'close', 'lowest', 'highest', 'ma5', 'ma10', 'ma15'],
+        source: [],
+    }
+
+    // MACD数据
+    const macdData: Dataset<MACD> = {
+        dimensions: ['date', 'macd', 'dif', 'dea'],
+        source: [],
+    }
+
+    // VOL数据
+    const volData: Dataset<VOL> = {
+        dimensions: ['date', 'vol'],
+        source: [],
+    }
+
+    // KDJ线数据
+    const kdjData: Dataset<KDJ> = {
+        dimensions: ['date', 'k', 'd', 'j'],
+        source: [],
+    }
+
+    // VOL线数据
+    const cciData: Dataset<CCI> = {
+        dimensions: ['date', 'cci'],
+        source: [],
+    }
+
+    // 处理行情数据
+    const handleData = (rawData: QueryHistoryDatasRsp[]) => {
+        invalidData.length = 0;
+        for (let i = 0; i < rawData.length; i++) {
+            const { o, c, h, l, ts, f, tv } = rawData[i];
+            const date = moment(ts).format('YYYY-MM-DD HH:mm:ss');
+
+            if (f) invalidData.push(i); // 添加补充数据的索引位置
+
+            klineData.source.push({
+                date,
+                open: o,
+                close: c,
+                lowest: l,
+                highest: h,
+                ma5: '-',
+                ma10: '-',
+                ma15: '-',
+            })
+
+            macdData.source.push({
+                date,
+                ema12: 0,
+                ema26: 0,
+                dif: 0,
+                dea: 0,
+                macd: 0,
+            })
+
+            volData.source.push({
+                date,
+                vol: tv,
+            })
+
+            kdjData.source.push({
+                date,
+                k: '-',
+                d: '-',
+                j: '-'
+            })
+
+            cciData.source.push({
+                date,
+                cci: '-',
+            })
+        }
+        // 计算各种指标
+        calcIndicator();
+    }
+
+    // 计算MA
+    const calcMA = (key: keyof Candlestick, count: number, startIndex = 0) => {
+        type T = Candlestick[keyof Candlestick];
+        const klineSource = klineData.source;
+
+        for (let i = startIndex; i < klineSource.length; i++) {
+            // 判断是否补充数据
+            if (invalidData.includes(i)) {
+                const value = klineSource.length > 0 ? klineSource[i - 1][key].toString() : '-'; // 如果存在,取上一条记录的MA值
+                (<T>klineSource[i][key]) = value;
+            } else {
+                let n = 0;
+                const tmpList: Candlestick[] = [];
+
+                for (let j = i; j >= 0; j--) {
+                    if (invalidData.includes(j)) continue; // 如果是补充数据,跳过本次循环
+                    if (n === count) break; // 如果 n 等于计数,结束循环
+                    tmpList.push(klineSource[j]);
+                    n++;
+                }
+
+                if (n === count) {
+                    // 计算总价(收盘价总和)
+                    const total = tmpList.reduce((res, e) => res + e.close, 0);
+                    // 计算均价
+                    (<T>klineSource[i][key]) = (total / count).toFixed(2);
+                } else {
+                    (<T>klineSource[i][key]) = '-';
+                }
+            }
+        }
+    }
+
+    // 计算EMA
+    const calcEMA = (key: keyof MACD, count: number, startIndex = 0) => {
+        type T = MACD[keyof MACD];
+        const macdSource = macdData.source;
+        const klineSource = klineData.source;
+        const a = 2 / (count + 1); // 平滑系数
+
+        for (let i = startIndex; i < klineSource.length; i++) {
+            const close = klineSource[i].close; // 收盘价
+
+            if (i === 0) {
+                (<T>macdSource[i][key]) = close; // 初始EMA取收盘价
+            } else {
+                const prevEMA = macdSource[i - 1][key]; // 昨日EMA
+                const value = a * close + (1 - a) * Number(prevEMA);
+
+                (<T>macdSource[i][key]) = value;
+            }
+        }
+    }
+
+    // 计算MACD
+    const calcMACD = (startIndex = 0) => {
+        // 先计算EMA
+        calcEMA('ema12', 12, startIndex); // EMA(12) = 2 ÷ 13 * 今日收盘价(12) + 11 ÷ 13 * 昨日EMA(12)
+        calcEMA('ema26', 26, startIndex); // EMA(12) = 2 ÷ 13 * 今日收盘价(12) + 11 ÷ 13 * 昨日EMA(12)
+
+        const macdSource = macdData.source;
+
+        for (let i = startIndex; i < macdSource.length; i++) {
+            const { ema12, ema26 } = macdSource[i];
+            const prevDEA = i > 0 ? macdSource[i - 1].dea : 0; // 昨日DEA
+
+            const dif = ema12 - ema26; // DIF = 今日EMA(12) - 今日EMA(26)
+            const dea = prevDEA * 8 / 10 + dif * 2 / 10; // DEA = 昨日DEA × 8 ÷ 10 + 今日DIF × 2 ÷ 10
+            const macd = (dif - dea) * 2; // MACD = (DIFF - DEA) × 2
+
+            macdSource[i].dif = Number(dif.toFixed(2));
+            macdSource[i].dea = Number(dea.toFixed(2));
+            macdSource[i].macd = Number(macd.toFixed(2));
+        }
+    }
+
+    // 计算KDJ
+    const calcKDJ = (startIndex = 0) => {
+        const count = 9; // 以9日周期计算
+        const klineSource = klineData.source;
+        const kdjSource = kdjData.source;
+
+        for (let i = startIndex; i < klineSource.length; i++) {
+            let n = 0;
+            const tmpList: Candlestick[] = [];
+
+            for (let j = i; j >= 0; j--) {
+                if (n === count) break; // 如果 n 等于计数,结束循环
+                tmpList.push(klineSource[j]);
+                n++;
+            }
+
+            if (n === count) {
+                const close = klineSource[i].close, // 收盘价
+                    n9 = tmpList.map((item) => item.close),
+                    h9 = Math.max(...n9), // 9天内最高价
+                    l9 = Math.min(...n9); // 9天内最低价
+
+                const rsv = (close - l9) / (h9 - l9) * 100, // RSV = (Ct-L9) ÷ (H9-L9) * 100
+                    prevK = Number(kdjSource[i - 1].k), // 昨日K值
+                    prevD = Number(kdjSource[i - 1].d); // 昨日D值
+
+                const kValue = isNaN(prevK) ? 50 : prevK; // 若无昨日K值,则可用50来代替
+                const dValue = isNaN(prevD) ? 50 : prevD; // 若无昨日D值,则可用50来代替
+
+                const k = (2 / 3) * kValue + (1 / 3) * rsv, // K = 2 ÷ 3 × 昨日K值 + 1 ÷ 3 × RSV
+                    d = (2 / 3) * dValue + (1 / 3) * k, // D = 2 ÷ 3 × 昨日D值 + 1 ÷ 3 × K值
+                    j = 3 * k - 2 * d; // J = 3 * K值 - 2 * D值
+
+                kdjSource[i].k = k.toFixed(2);
+                kdjSource[i].d = d.toFixed(2);
+                kdjSource[i].j = j.toFixed(2);
+            }
+        }
+    }
+
+    // 计算CCI
+    const calcCCI = (startIndex = 0) => {
+        const count = 14; // 以14日周期计算
+        const klineSource = klineData.source;
+        const cciSource = cciData.source;
+
+        for (let i = startIndex; i < klineSource.length; i++) {
+            let n = 0;
+            const tmpList: Candlestick[] = [];
+
+            for (let j = i; j >= 0; j--) {
+                if (n === count) break; // 如果 n 等于计数,结束循环
+                tmpList.push(klineSource[j]);
+                n++;
+            }
+
+            if (n === count) {
+                const calcTP = (item: Candlestick) => (item.highest + item.lowest + item.close) / 3; // TP = (最高价 + 最低价 + 收盘价) ÷ 3
+                const tp = calcTP(klineSource[i]),
+                    ma = tmpList.reduce((res, e) => res + calcTP(e), 0) / count, // MA = 近N日TP的累计之和 ÷ N
+                    md = tmpList.reduce((res, e) => res + Math.abs(calcTP(e) - ma), 0) / count; // MD = 近N日TP的绝对值的累计之和 ÷ N
+
+                const cci = (tp - ma) / (md * 0.015);
+                cciSource[i].cci = cci.toFixed(2);
+            }
+        }
+    }
+
+    // 计算各种指标
+    const calcIndicator = (startIndex = 0) => {
+        calcMA('ma5', 5, startIndex);
+        calcMA('ma10', 10, startIndex);
+        calcMA('ma15', 15, startIndex);
+        calcMACD(startIndex);
+        calcKDJ(startIndex);
+        calcCCI(startIndex);
+    }
+
+    return {
+        klineData,
+        macdData,
+        volData,
+        kdjData,
+        cciData,
+        handleData,
+        calcIndicator,
+    }
+}

+ 57 - 0
src/common/components/echarts/echarts-kline/index.less

@@ -0,0 +1,57 @@
+.mtp-echats-kline {
+    display       : flex;
+    flex-direction: column;
+    height        : 100%;
+
+    &__container {
+        display       : flex;
+        flex-direction: column;
+
+        .mtp-echarts {
+            flex: 1;
+        }
+
+        .legend {
+            display        : flex;
+            color          : #7a8a94;
+            font-size      : 12px;
+            list-style-type: none;
+            padding        : 4px 4%;
+            margin         : 0;
+
+            &-item {
+                &:not(:first-child) {
+                    margin-left: 10px;
+                }
+            }
+        }
+    }
+
+    &__container.main {
+        flex: 2;
+    }
+
+    &__container.indicator {
+        position: relative;
+        flex    : 1;
+
+        .section {
+            flex          : 1;
+            display       : flex;
+            flex-direction: column;
+            overflow      : hidden;
+
+            &.is-hide {
+                position: absolute;
+                z-index : -1;
+                width   : 100%;
+                height  : 100%;
+                opacity : 0;
+            }
+        }
+    }
+
+    .mtp-tabbar {
+        padding: 4px 4%;
+    }
+}

+ 316 - 0
src/common/components/echarts/echarts-kline/index.vue

@@ -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>

+ 353 - 0
src/common/components/echarts/echarts-kline/options.ts

@@ -0,0 +1,353 @@
+import { ref, watch } from 'vue'
+import { EChartsOption } from 'echarts';
+import { Dataset, Candlestick, MACD, VOL, KDJ, CCI, Colors } from './type'
+import { getTheme, ThemeEnum } from '@/common/config/theme';
+import moment from 'moment';
+
+export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MACD>, volData: Dataset<VOL>, kdjData: Dataset<KDJ>, cciData: Dataset<CCI>) {
+    const theme = getTheme(),
+        klineOption = ref<EChartsOption>({}),
+        macdOption = ref<EChartsOption>({}),
+        volOption = ref<EChartsOption>({}),
+        kdjOption = ref<EChartsOption>({}),
+        cciOption = ref<EChartsOption>({});
+
+    const getDefaultOption = (): EChartsOption => {
+        const { xAxisLineColor } = getColors();
+        return {
+            dataZoom: {
+                type: 'inside',
+                startValue: klineData.source.length - 120, // 起始显示K线条数(最新120条)
+                endValue: klineData.source.length,
+                minValueSpan: 50, // 限制窗口缩放显示最少数据条数
+                maxValueSpan: 400, // 限制窗口缩放显示最大数据条数
+            },
+            xAxis: {
+                type: 'category',
+                axisLabel: {
+                    formatter: (val: string) => moment(val).format('YYYY/MM/DD'),
+                    margin: 12,
+                },
+            },
+            yAxis: {
+                scale: true,
+                splitLine: {
+                    lineStyle: {
+                        // 坐标分隔线颜色
+                        color: xAxisLineColor,
+                    },
+                },
+            },
+        }
+    }
+
+    // K线配置项
+    const setKlineOption = () => {
+        const { dimensions, source } = klineData;
+        klineOption.value = {
+            ...getDefaultOption(),
+            dataset: {
+                dimensions,
+                source,
+            },
+            series: [
+                {
+                    name: 'K线',
+                    type: 'candlestick',
+                    // 最新价标线
+                    markLine: {
+                        // 标线两端图标
+                        symbol: 'none',
+                        // 标线标签样式
+                        label: {
+                            color: '#444',
+                            fontWeight: 'bold',
+                            backgroundColor: 'rgba(255,255,255,.75)',
+                            padding: 5,
+                            borderRadius: 3
+                        },
+                        // 标线样式
+                        lineStyle: {
+                            type: 'dashed',
+                            color: 'rgba(255,255,255,.3)'
+                        },
+                        data: [
+                            {
+                                // 收盘价
+                                yAxis: source.length ? source[source.length - 1].close : 0,
+                            },
+                        ],
+                    },
+                },
+                {
+                    name: 'MA5',
+                    type: 'line',
+                    sampling: 'average', //折线图在数据量远大于像素点时候的降采样策略,开启后可以有效的优化图表的绘制效率
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+                {
+                    name: 'MA10',
+                    type: 'line',
+                    sampling: 'average',
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+                {
+                    name: 'MA15',
+                    type: 'line',
+                    sampling: 'average',
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+            ],
+        }
+    }
+
+    // MACD配置项
+    const setMacdOption = () => {
+        const { upColor, downColor } = getColors();
+        const { dimensions, source } = macdData;
+        macdOption.value = {
+            ...getDefaultOption(),
+            dataset: {
+                dimensions,
+                source,
+            },
+            series: [
+                {
+                    name: 'MACD',
+                    type: 'bar',
+                    sampling: 'average',
+                    barWidth: '20%',
+                    itemStyle: {
+                        color: (params: any) => {
+                            if (params.data.macd > 0) {
+                                return upColor;
+                            } else {
+                                return downColor;
+                            }
+                        },
+                    }
+                },
+                {
+                    name: 'DIF',
+                    type: 'line',
+                    sampling: 'average',
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+                {
+                    name: 'DEA',
+                    type: 'line',
+                    sampling: 'average',
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+            ],
+        }
+    }
+
+    // VOL配置项
+    const setVolOption = () => {
+        const { dimensions, source } = volData;
+        volOption.value = {
+            ...getDefaultOption(),
+            dataset: {
+                dimensions,
+                source,
+            },
+            series: [
+                {
+                    name: 'VOL',
+                    type: 'bar',
+                    sampling: 'average',
+                    barWidth: '60%',
+                },
+            ],
+        }
+    }
+
+    // KDJ配置项
+    const setKdjOption = () => {
+        const { dimensions, source } = kdjData;
+        kdjOption.value = {
+            ...getDefaultOption(),
+            dataset: {
+                dimensions,
+                source,
+            },
+            series: [
+                {
+                    name: 'K',
+                    type: 'line',
+                    sampling: 'average',
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+                {
+                    name: 'D',
+                    type: 'line',
+                    sampling: 'average',
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+                {
+                    name: 'J',
+                    type: 'line',
+                    sampling: 'average',
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+            ],
+        }
+    }
+
+    // CCI配置项
+    const setCciOption = () => {
+        const { dimensions, source } = cciData;
+        cciOption.value = {
+            ...getDefaultOption(),
+            dataset: {
+                dimensions,
+                source,
+            },
+            series: [
+                {
+                    name: 'CCI',
+                    type: 'line',
+                    sampling: 'average',
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+            ],
+        }
+    }
+
+    // 获取图表主题色
+    const getColors = (): Colors => {
+        switch (theme.value) {
+            case ThemeEnum.default:
+            case ThemeEnum.dark: {
+                return {
+                    upColor: '#eb5454',
+                    downColor: '#47b262',
+                    xAxisLineColor: '#171B1D',
+                    yAxisLineColor: '#171B1D',
+                };
+            }
+            case ThemeEnum.light: {
+                return {
+                    upColor: '#eb5454',
+                    downColor: '#47b262',
+                    xAxisLineColor: '#DAE5EC',
+                    yAxisLineColor: '#DAE5EC',
+                };
+            }
+        }
+    }
+
+    const initOptions = () => {
+        setKlineOption();
+        setMacdOption();
+        setVolOption();
+        setKdjOption();
+        setCciOption();
+    }
+
+    // 动态更新数据
+    const updateOptions = () => {
+        const klineSource = klineData.source;
+        const macdSource = macdData.source;
+        const volSource = volData.source;
+        const kdjSource = kdjData.source;
+        const cciSource = cciData.source;
+
+        klineOption.value = {
+            dataset: {
+                source: klineSource,
+            },
+            series: [
+                {
+                    name: 'K线',
+                    markLine: {
+                        data: [
+                            {
+                                yAxis: klineSource[klineSource.length - 1].close,
+                            },
+                        ],
+                    },
+                },
+            ],
+        }
+        macdOption.value = {
+            dataset: {
+                source: macdSource,
+            },
+        }
+        volOption.value = {
+            dataset: {
+                source: volSource,
+            },
+        }
+        kdjOption.value = {
+            dataset: {
+                source: kdjSource,
+            },
+        }
+        cciOption.value = {
+            dataset: {
+                source: cciSource,
+            },
+        }
+    }
+
+    // 监听主题变化
+    watch(theme, () => initOptions());
+
+    return {
+        klineOption,
+        macdOption,
+        volOption,
+        kdjOption,
+        cciOption,
+        initOptions,
+        updateOptions,
+    }
+}

+ 93 - 0
src/common/components/echarts/echarts-kline/type.ts

@@ -0,0 +1,93 @@
+/**
+ * 周期类型
+ */
+export enum CycleType {
+    time = -1, // 分时
+    minutes = 1, // 1分钟
+    minutes5 = 2, // 5分钟
+    minutes30 = 3, // 30分钟
+    minutes60 = 4, // 60分钟
+    hours2 = 120, // 2小时
+    Hours4 = 240, // 4小时
+    days = 11, // 日线
+}
+
+/**
+ * 指标类型
+ */
+export enum SeriesType {
+    MACD = 'MACD',
+    VOL = 'VOL',
+    KDJ = 'KDJ',
+    CCI = 'CCI',
+}
+
+/**
+ * 图表数据集
+ */
+export type Dataset<T> = {
+    dimensions: (keyof T)[],
+    source: T[],
+}
+
+/**
+ * K线
+ */
+export type Candlestick = {
+    date: string, // xAxis数据,必须是第一个属性
+    open: number, // 开盘
+    close: number, // 收盘
+    lowest: number, // 最低
+    highest: number, //最高
+    ma5: string,
+    ma10: string,
+    ma15: string,
+}
+
+/**
+ * MACD
+ */
+export type MACD = {
+    date: string, // xAxis数据,必须是第一个属性
+    macd: number,
+    dif: number,
+    dea: number,
+    ema12: number,
+    ema26: number,
+}
+
+/**
+ * VOL
+ */
+export type VOL = {
+    date: string, // xAxis数据,必须是第一个属性
+    vol: number,
+}
+
+/**
+ * KDJ
+ */
+export type KDJ = {
+    date: string, // xAxis数据,必须是第一个属性
+    k: string,
+    d: string,
+    j: string,
+}
+
+/**
+ * CCI
+ */
+export type CCI = {
+    date: string, // xAxis数据,必须是第一个属性
+    cci: string,
+}
+
+/**
+ * 图表主题色
+ */
+export type Colors = {
+    upColor: string,
+    downColor: string,
+    xAxisLineColor: string,
+    yAxisLineColor: string,
+}

+ 128 - 0
src/common/components/echarts/echarts-timeline/dataset.ts

@@ -0,0 +1,128 @@
+import { QueryTSDataRsp } from '@/services/go/quote/interface';
+import { State } from './type';
+import { getRangeTime } from '@/utils/time';
+import moment from 'moment';
+
+export function useDataset() {
+    // 行情历史数据中所有补充数据的索引位置(用于指标计算)
+    const invalidData: number[] = [];
+    const state: State = {
+        rawDate: [],
+        yestClose: 0,
+        decimal: 0,
+        maxMark: 0,
+        minMark: 0,
+        interval: 0,
+        dataset: {
+            dimensions: ['date', 'close', 'ma5'],
+            source: {
+                date: [],
+                close: [],
+                ma5: []
+            },
+        }
+    }
+
+    // 处理行情数据
+    const handleData = (rawData: QueryTSDataRsp) => {
+        const { date, close, ma5 } = state.dataset.source;
+        invalidData.length = 0;
+        state.decimal = rawData.decimalPlace;
+        state.yestClose = rawData.preSettle;
+
+        // 开盘交易时间
+        for (let i = 0; i < rawData.runSteps.length; i++) {
+            const { start, end } = rawData.runSteps[i];
+            const rangeTime = getRangeTime(start, end, 'HH:mm', 'm');
+            date.push(...rangeTime);
+        }
+
+        for (let i = 0; i < rawData.historyDatas.length; i++) {
+            const { ts, c, f } = rawData.historyDatas[i];
+            const date = moment(ts).format('YYYY-MM-DD HH:mm:ss');
+
+            if (f) invalidData.push(i); // 添加补充数据的索引位置
+
+            state.rawDate.push(date);
+            close.push(c);
+            ma5.push('-');
+        }
+        console.log(state.dataset.source)
+        // 计算各种指标
+        calcIndicator();
+    }
+
+    // 计算MA
+    const calcMA = (startIndex = 0) => {
+        const { close, ma5 } = state.dataset.source;
+        for (let i = startIndex; i < close.length; i++) {
+            // 判断是否补充数据
+            if (invalidData.includes(i)) {
+                ma5[i] = i > 0 ? ma5[i - 1].toString() : '--'; // 如果存在,取上一条记录的MA值
+            } else {
+                let n = 0;
+                const tmpList: number[] = [];
+
+                for (let j = i; j >= 0; j--) {
+                    if (invalidData.includes(j)) continue; // 如果是补充数据,跳过本次循环
+                    if (n === 5) break; // 如果 n 等于计数,结束循环
+                    tmpList.push(close[j]);
+                    n++;
+                }
+
+                if (n === 5) {
+                    // 计算总价(收盘价总和)
+                    const total = tmpList.reduce((res, val) => res + val, 0);
+                    // 计算均价
+                    ma5[i] = (total / 5).toFixed(2);
+                } else {
+                    ma5[i] = '--';
+                }
+            }
+        }
+    }
+
+    // 计算图表最高低指标线
+    const calcMarkLine = () => {
+        const { close } = state.dataset.source;
+        let max = Math.max(...close); // 取历史行情最高价
+        let min = Math.min(...close); // 取历史行情最低价
+
+        const last = close[close.length - 1], // 历史行情最后收盘价
+            a = state.yestClose - min, // 计算收盘价和最低价的差值
+            b = max - state.yestClose; // 计算收盘价和最高价的差值
+
+        // 比较差值大小
+        if (a > b) {
+            max = state.yestClose + a;
+            if (last > max) {
+                const c = last - max;
+                max += c;
+                min -= c;
+            }
+        } else {
+            min = min - (b - a);
+            if (min > last) {
+                const c = min - last;
+                max += c;
+                min -= c;
+            }
+        }
+
+        state.maxMark = max + min * 0.01;
+        state.minMark = min - min * 0.01;
+        state.interval = (state.maxMark - state.minMark) / 6;
+    };
+
+    // 计算各种指标
+    const calcIndicator = (startIndex = 0) => {
+        calcMA(startIndex);
+        calcMarkLine();
+    }
+
+    return {
+        state,
+        handleData,
+        calcIndicator,
+    }
+}

+ 19 - 0
src/common/components/echarts/echarts-timeline/index.less

@@ -0,0 +1,19 @@
+.mtp-echats-timeline {
+    display       : flex;
+    flex-direction: column;
+    height        : 100%;
+
+    .legend {
+        display        : flex;
+        color          : #7a8a94;
+        font-size      : 12px;
+        list-style-type: none;
+        padding        : 4px 4%;
+
+        &-item {
+            &:not(:first-child) {
+                margin-left: 10px;
+            }
+        }
+    }
+}

+ 137 - 0
src/common/components/echarts/echarts-timeline/index.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="mtp-echats-timeline">
+    <ul class="legend">
+      <li class="legend-item">收: {{timeDetail && timeDetail.close}}</li>
+      <li class="legend-item">MA5: {{timeDetail && timeDetail.ma5}}</li>
+    </ul>
+    <mtp-echarts :option="timeOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="initData" />
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, watch, computed } from 'vue'
+import { QueryQuoteDayRsp } from '@/services/go/quote/interface';
+import { QueryTSData } from '@/services/go/quote';
+import { getQuoteDayInfoByCode } from '@/services/bus/goods';
+import { throttle } from '@/utils/time'
+import { useDataset } from './dataset'
+import { useOptions } from './options'
+import moment from 'moment';
+import MtpEcharts from '../../echarts/echarts-base/index.vue'
+
+export default defineComponent({
+  emits: ['ready'],
+  components: {
+    MtpEcharts,
+  },
+  props: {
+    goodscode: {
+      type: String,
+      default: '',
+    },
+  },
+  setup(props, { emit }) {
+    const loading = ref(false),
+      showEmpty = ref(false),
+      dataIndex = ref(0), // 当前数据索引值
+      quote = ref<QueryQuoteDayRsp>(getQuoteDayInfoByCode(props.goodscode)!); // 商品实时行情
+
+    const { state, handleData, calcIndicator } = useDataset();
+    const { timeOption, initOptions, updateOptions } = useOptions(state);
+    const timeDetail = computed(() => {
+      const { close, ma5 } = state.dataset.source;
+      return {
+        close: close[dataIndex.value] ?? '--',
+        ma5: ma5[dataIndex.value] ?? '--',
+      }
+    });
+
+    // 初始化数据
+    const initData = () => {
+      showEmpty.value = false;
+      loading.value = true;
+      state.dataset.source = {
+        date: [],
+        close: [],
+        ma5: []
+      }
+
+      // 查询历史数据
+      QueryTSData(props.goodscode).then((res) => {
+        if (res.historyDatas.length) {
+          dataIndex.value = res.historyDatas.length - 1;
+          handleData(res);
+          initOptions();
+        } else {
+          showEmpty.value = true;
+        }
+
+        // 调用父级函数查询tik数据 (不合理的逻辑处理,待优化)
+        emit('ready', res.startTime, res.endTime);
+      }).catch((err) => {
+        console.error(err);
+        showEmpty.value = true;
+      }).finally(() => {
+        loading.value = false;
+      });
+    }
+
+    // 更新图表数据
+    const updateChartData = () => {
+      const { close, ma5 } = state.dataset.source,
+        lastIndex = close.length - 1, // 历史行情最后索引位置
+        cycleMilliseconds = 60 * 1000, // 一分钟毫秒数
+        newTime = moment(quote.value.lasttime), // 实时行情最新时间
+        newPrice = quote.value.last; // 实时行情最新价
+
+      const oldTime = lastIndex === -1 ? newTime : moment(state.rawDate[lastIndex]); // 历史行情最后时间
+      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');
+
+          // 新增分时数据
+          state.rawDate.push(newDate);
+          close.push(newPrice);
+          ma5.push('-')
+        } else {
+          close[lastIndex] = newPrice; // 更新最后一条记录的收盘价
+        }
+
+        // 更新各种指标
+        calcIndicator(lastIndex === -1 ? 0 : lastIndex);
+
+        // 延迟图表更新,减少卡顿
+        throttle(() => {
+          updateOptions();
+        }, 1000)
+      }
+    }
+
+    // 监听行情推送
+    watch(() => quote.value.last, () => {
+      if (!loading.value) {
+        updateChartData();
+      }
+    })
+
+    return {
+      loading,
+      showEmpty,
+      dataIndex,
+      timeOption,
+      timeDetail,
+      initData,
+    }
+  }
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 239 - 0
src/common/components/echarts/echarts-timeline/options.ts

@@ -0,0 +1,239 @@
+import { ref, watch } from 'vue'
+import { toDecimalFull } from '@/utils/number';
+import * as echarts from 'echarts';
+import { State, Colors } from './type'
+import { getTheme, ThemeEnum } from '@/common/config/theme';
+
+export function useOptions(state: State) {
+    const theme = getTheme(),
+        timeOption = ref<echarts.EChartsOption>({});
+
+    // 获取图表主题色
+    const getColors = (): Colors => {
+        switch (theme.value) {
+            case ThemeEnum.default:
+            case ThemeEnum.dark: {
+                return {
+                    upColor: '#FF2B2B',
+                    downColor: '#1FF195',
+                    xAxisLineColor: '#171B1D',
+                    yAxisLineColor: '#171B1D',
+                    seriesLineColor: '#39afe6',
+                    seriesAreaGradients: new echarts.graphic.LinearGradient(0, 0, 0, 1,
+                        [
+                            {
+                                offset: 0,
+                                color: 'rgba(0, 136, 212, 0.3)',
+                            },
+                            {
+                                offset: 1,
+                                color: 'rgba(0, 136, 212, 0.3)',
+                            },
+                        ],
+                        false
+                    ),
+                };
+            }
+            case ThemeEnum.light: {
+                return {
+                    upColor: '#FF2B2B',
+                    downColor: '#00A843',
+                    xAxisLineColor: '#DAE5EC',
+                    yAxisLineColor: '#DAE5EC',
+                    seriesLineColor: '#3864d7',
+                    seriesAreaGradients: new echarts.graphic.LinearGradient(0, 0, 0, 1,
+                        [
+                            {
+                                offset: 0,
+                                color: 'rgba(0, 136, 212, 0.3)',
+                            },
+                            {
+                                offset: 1,
+                                color: 'rgba(0, 136, 212, 0.3)',
+                            },
+                        ],
+                        false
+                    ),
+                };
+            }
+        }
+    }
+
+    const initOptions = () => {
+        const colors = getColors();
+        const { dataset, maxMark, minMark, interval, yestClose, decimal } = state;
+
+        timeOption.value = {
+            dataset,
+            axisPointer: {
+                label: {
+                    // 小数点精度
+                    precision: decimal,
+                }
+            },
+            xAxis: {
+                type: 'category',
+                axisLabel: {
+                    showMinLabel: true,
+                    showMaxLabel: true,
+                    margin: 12,
+                },
+            },
+            yAxis: [
+                {
+                    id: 'leftY',
+                    max: maxMark,
+                    min: minMark,
+                    interval,
+                    axisLabel: {
+                        formatter: (val: number) => toDecimalFull(val, decimal),
+                        color: (val: any) => {
+                            const num = Number(val).toFixed(decimal);
+                            if (Number(num) > yestClose) return colors.upColor;
+                            if (Number(num) < yestClose) return colors.downColor;
+                            return '#7a8a94';
+                        },
+                    },
+                    splitLine: {
+                        lineStyle: {
+                            // 坐标分隔线颜色
+                            color: colors.xAxisLineColor,
+                        },
+                    },
+                },
+                {
+                    id: 'rightY',
+                    max: maxMark,
+                    min: minMark,
+                    interval,
+                    axisLabel: {
+                        formatter: (val: number) => calcRatio(val),
+                        color: (val: any) => {
+                            const num = Number(val).toFixed(decimal);
+                            if (Number(num) > yestClose) return colors.upColor;
+                            if (Number(num) < yestClose) return colors.downColor;
+                            return '#7a8a94';
+                        },
+                    },
+                    axisPointer: {
+                        show: false,
+                    },
+                    splitLine: {
+                        show: false,
+                    },
+                }
+            ],
+            // series 中不指定 yAxisId 或 yAxisIndex 默认关联 yAxis 第一个配置,xAxis 配置同理
+            series: [
+                {
+                    name: '分时',
+                    type: 'line',
+                    yAxisId: 'leftY',
+                    smooth: true,
+                    symbol: 'none', //中时有小圆点
+                    lineStyle: {
+                        color: colors.seriesLineColor,
+                        opacity: 0.8,
+                        width: 1,
+                    },
+                    areaStyle: {
+                        color: colors.seriesAreaGradients,
+                        shadowColor: 'rgba(0, 0, 0, 0.1)',
+                        shadowBlur: 10,
+                    },
+                    // 标线
+                    markLine: {
+                        // 标线两端图标
+                        symbol: 'none',
+                        // 标线标签样式
+                        label: {
+                            color: '#444',
+                            fontWeight: 'bold',
+                            backgroundColor: 'rgba(255,255,255,.75)',
+                            padding: 5,
+                            borderRadius: 3
+                        },
+                        // 标线样式
+                        lineStyle: {
+                            type: 'dashed',
+                        },
+                        data: [
+                            {
+                                yAxis: dataset.source.close[dataset.source.close.length - 1] ?? '--', // 最新价
+                                lineStyle: {
+                                    color: 'rgba(255,255,255,.5)'
+                                },
+                            },
+                            {
+                                yAxis: yestClose, // 昨日收盘价
+                            },
+                        ],
+                    },
+                },
+                {
+                    name: 'MA5',
+                    type: 'line',
+                    sampling: 'average', //折线图在数据量远大于像素点时候的降采样策略,开启后可以有效的优化图表的绘制效率
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+            ],
+        }
+    }
+
+    // 动态更新数据
+    const updateOptions = () => {
+        const { dataset, yestClose } = state;
+        timeOption.value = {
+            dataset: {
+                source: dataset.source,
+            },
+            series: [
+                {
+                    name: '分时',
+                    markLine: {
+                        data: [
+                            {
+                                yAxis: dataset.source.close[dataset.source.close.length - 1] ?? '--', // 最新价
+                                lineStyle: {
+                                    color: 'rgba(255,255,255,.5)'
+                                },
+                            },
+                            {
+                                yAxis: yestClose, // 昨日收盘价
+                            },
+                        ],
+                    },
+                },
+            ],
+        }
+    }
+
+    // 计算涨跌幅百分比,涨跌幅=(今日收盘价-昨日收盘价)/昨日收盘价*100%
+    const calcRatio = (val: number) => {
+        const yestclose = state.yestClose;
+        const num = Number(val);
+
+        if (isNaN(num)) {
+            return '-';
+        }
+        if (yestclose > 0) {
+            const result = (num - yestclose) / yestclose * 100;
+            return toDecimalFull(result) + '%';
+        }
+        return '0%';
+    }
+
+    // 监听主题变化
+    watch(theme, () => initOptions());
+
+    return {
+        timeOption,
+        initOptions,
+        updateOptions,
+    }
+}

+ 38 - 0
src/common/components/echarts/echarts-timeline/type.ts

@@ -0,0 +1,38 @@
+import { LinearGradientObject } from 'echarts';
+
+/**
+ * 状态数据
+ */
+export type State = {
+    rawDate: string[], // 原始日期
+    yestClose: number, // 昨日收盘价
+    decimal: number, // 小数位
+    maxMark: number, // Y轴最大刻度
+    minMark: number, // Y轴最小刻度
+    interval: number, // Y轴间隔高度
+    dataset: {
+        dimensions: (keyof Timeline)[],
+        source: Timeline,
+    }
+}
+
+/**
+ * 分时线
+ */
+export type Timeline = {
+    date: string[], // xAxis数据,必须是第一个属性
+    close: number[],
+    ma5: string[],
+}
+
+/**
+ * 图表主题色
+ */
+export type Colors = {
+    upColor: string,
+    downColor: string,
+    xAxisLineColor: string,
+    yAxisLineColor: string,
+    seriesLineColor: string,
+    seriesAreaGradients: LinearGradientObject,
+}

+ 30 - 0
src/common/components/tabbar/index.less

@@ -0,0 +1,30 @@
+.mtp-tabbar {
+    &--menu {
+        .list {
+            display        : flex;
+            list-style-type: none;
+            margin         : 0;
+            padding        : 0;
+
+            &-item {
+                display        : flex;
+                justify-content: center;
+                align-items    : center;
+                height         : 22px;
+                color          : #7a8a94;
+                cursor         : pointer;
+                border         : 1px solid #22292c;
+                padding        : 0 16px;
+
+                &:not(:first-child) {
+                    border-left: 0;
+                }
+
+                &.active {
+                    color           : #0866b8;
+                    background-color: #0e2f4c;
+                }
+            }
+        }
+    }
+}

+ 48 - 0
src/common/components/tabbar/index.vue

@@ -0,0 +1,48 @@
+<!-- 标签栏组件 -->
+<template>
+  <div :class="['mtp-tabbar', 'mtp-tabbar--' + theme]">
+    <ul class="list">
+      <li :class="['list-item', active === index && 'active']" v-for="(item,index) in data" :key="index" @click="onChange(index)">
+        {{item.label}}
+      </li>
+    </ul>
+    <slot></slot>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue'
+import { Tabbar } from './type'
+
+export default defineComponent({
+  emits: ['update:active', 'change'],
+  props: {
+    data: {
+      type: Array as PropType<Tabbar[]>,
+      default: () => ([]),
+    },
+    active: {
+      type: Number,
+      default: 0,
+    },
+    theme: {
+      type: String as PropType<'bar' | 'menu'>,
+      default: 'bar',
+    }
+  },
+  setup(props, { emit }) {
+    const onChange = (index: number) => {
+      emit('update:active', index);
+      emit('change', index);
+    }
+
+    return {
+      onChange
+    }
+  }
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 7 - 0
src/common/components/tabbar/type.ts

@@ -0,0 +1,7 @@
+/**
+ * 标签栏
+ */
+export interface Tabbar {
+    code: string;
+    label: string;
+}

+ 29 - 27
src/hooks/account/index.ts

@@ -28,37 +28,39 @@ const tradeAccount = ref<TradeAccount>();
 export function useTradeAccount() {
     // 获取资金账户列表
     const getTradeAccountList = () => {
-        tradeAccountList.length = 0;
-        loading.value = true;
-
-        // 登录ID
-        const loginID = Number(geLoginID_number());
-
-        getTaAccounts({ loginID }).then(async (res) => {
-            for (let i = 0; i < res.length; i++) {
-                const account = res[i];
-                const positionList: TradePosition[] = [];
-
-                // 获取账户下的期货持仓列表
-                await queryErmcpTradePosition({ accountID: account.accountid }).then((res) => {
-                    res.forEach((item) => {
-                        positionList.push({
-                            ...item,
-                            ...calcPositionValue(item),
+        if (!loading.value) {
+            tradeAccountList.length = 0;
+            loading.value = true;
+
+            // 登录ID
+            const loginID = Number(geLoginID_number());
+
+            getTaAccounts({ loginID }).then(async (res) => {
+                for (let i = 0; i < res.length; i++) {
+                    const account = res[i];
+                    const positionList: TradePosition[] = [];
+
+                    // 获取账户下的期货持仓列表
+                    await queryErmcpTradePosition({ accountID: account.accountid }).then((res) => {
+                        res.forEach((item) => {
+                            positionList.push({
+                                ...item,
+                                ...calcPositionValue(item),
+                            })
                         })
                     })
-                })
 
-                tradeAccountList.push({
-                    positionList,
-                    ...account,
-                    ...calcCapitalValue(account, positionList),
-                })
-            }
+                    tradeAccountList.push({
+                        positionList,
+                        ...account,
+                        ...calcCapitalValue(account, positionList),
+                    })
+                }
 
-            tradeAccountChange(getSelectedAccountId());
-            loading.value = false;
-        })
+                tradeAccountChange(getSelectedAccountId());
+                loading.value = false;
+            })
+        }
     }
 
     // 切换资金账户

+ 2 - 0
src/services/go/ermcp/futures/interface.ts

@@ -69,6 +69,7 @@ export interface QueryErmcpOrderDetailsRsp {
     pricemode: number;//取价方式 - 1:市价 2: 限价
     tradedate: string;//交易日(yyyyMMdd)
     tradeqty: number;//成交数量
+    decimalplace: number;
 }
 
 /**
@@ -112,4 +113,5 @@ export interface QueryErmcpTradeDetailsRsp {
     tradeqty: number;//成交数量
     tradetime: string;//成交时间
     tradetype: number;//成交类别 - 1:正常委托成交 2:风控斩仓成交 3:修正持仓成交 4:管理端斩仓成交
+    decimalplace: number;
 }

+ 2 - 2
src/views/market/futures/compoments/futures-trade/index.vue

@@ -295,8 +295,8 @@ export default defineComponent({
           break;
         }
         case 'close': {
-          successMsg = '平仓成功';
-          failMsg = '平仓失败';
+          successMsg = '平仓委托成功';
+          failMsg = '平仓委托失败';
 
           if (!selectedPosition.value) {
             message.error('请选择需要平仓的单据');

+ 3 - 1
src/views/market/spot_trade/components/goods-chart/chart/index.less

@@ -24,13 +24,15 @@
         flex-direction: column;
         flex          : 1;
         height        : 100%;
+        padding       : 10px 0;
 
         &__main {
             flex: 1;
         }
 
         &__header {
-            padding: 4px 0;
+            padding      : 4px 4%;
+            margin-bottom: 10px;
         }
 
         &__tabs {

+ 101 - 107
src/views/market/spot_trade/components/goods-chart/chart/index.vue

@@ -3,18 +3,12 @@
   <div class="chart-container">
     <div class="chart-content">
       <div class="chart-content__header">
-        <a-menu class="chart-content__tabs" v-model:selectedKeys="activeCycleName" mode="horizontal" @click="changeCycleType">
-          <a-menu-item v-for="item in chartType" :key="item.name">{{ item.label }}</a-menu-item>
-        </a-menu>
-        <a-menu class="chart-content__tabs" v-model:selectedKeys="activeSeriesType" mode="horizontal" v-if="activeCycleType !== CycleType.time">
-          <a-menu-item key="MACD">MACD</a-menu-item>
-          <a-menu-item key="VOL">VOL</a-menu-item>
-          <a-menu-item key="KDJ">KDJ</a-menu-item>
-          <a-menu-item key="CCI">CCI</a-menu-item>
-        </a-menu>
+        <mtp-tabbar theme="menu" :data="chartType" v-model:active="activeCycleType" @change="changeCycleType" />
       </div>
-      <echart-time class="chart-content__main" :quote-data="selectedRow" @change="getHistoryTikDatas" v-if="activeCycleType === CycleType.time"></echart-time>
-      <echart-kline class="chart-content__main" :quote-data="selectedRow" :cycle-type="activeCycleType" :series-type="activeSeriesType[0]" v-else></echart-kline>
+      <!-- K线  -->
+      <mtp-echarts-timeline class="chart-content__main" :goodscode="selectedRow.goodscode" @ready="getHistoryTikDatas" v-if="chartType[activeCycleType].code === CycleType.time" />
+      <!-- 分时线  -->
+      <mtp-echarts-kline class="chart-content__main" :cycle-type="activeCycleType" :goodscode="selectedRow.goodscode" v-else />
       <div class="chart-content__footer"></div>
     </div>
     <div class="chart-slider">
@@ -136,118 +130,118 @@
 import { defineComponent } from '@/common/export/commonTable';
 import { _closeModal } from '@/common/setup/modal/modal';
 import { PropType, ref, watch } from 'vue';
-import { QueryQuoteDayRsp, QueryHistoryTikDatasRsp, CycleType } from '@/services/go/quote/interface';
+import { QueryQuoteDayRsp, QueryHistoryTikDatasRsp } from '@/services/go/quote/interface';
 import { QueryHistoryTikDatas } from '@/services/go/quote';
 import { formatTime } from '@/common/methods';
 import { changeUnit } from '@/utils/qt/common';
 import { ComponentType } from '@/views/market/spot_trade/spot_trade_order_transaction/setup';
-import { EchartKline, EchartTime } from '@/common/components/echart';
 import { handleQuotePriceColor, quoteChange, handleNoneValue, quoteAmplitude } from '@/common/setup/table/tableQuote';
 import moment from 'moment';
+import { CycleType } from '@/common/components/echarts/echarts-kline/type';
+import MtpEchartsKline from '@/common/components/echarts/echarts-kline/index.vue';
+import MtpEchartsTimeline from '@/common/components/echarts/echarts-timeline/index.vue';
+import MtpTabbar from '@/common/components/tabbar/index.vue';
 
 export default defineComponent({
-    emits: ['cancel', 'update'],
-    name: 'stock-exchange',
-    components: {
-        EchartKline,
-        EchartTime,
+  emits: ['cancel', 'update'],
+  name: 'stock-exchange',
+  components: {
+    MtpEchartsKline,
+    MtpEchartsTimeline,
+    MtpTabbar,
+  },
+  props: {
+    selectedRow: {
+      type: Object as PropType<QueryQuoteDayRsp>,
+      default: {},
     },
-    props: {
-        selectedRow: {
-            type: Object as PropType<QueryQuoteDayRsp>,
-            default: {},
-        },
-        showExchange: {
-            type: Boolean,
-            default: true,
-        },
+    showExchange: {
+      type: Boolean,
+      default: true,
     },
-    setup(props, context) {
-        const { visible, cancel } = _closeModal(context),
-            activeCycleName = ref<string[]>(['time']),
-            activeCycleType = ref<CycleType>(CycleType.time),
-            activeSeriesType = ref<string[]>(['MACD']),
-            tradedList = ref<QueryHistoryTikDatasRsp[]>([]);
+  },
+  setup(props, context) {
+    const { visible, cancel } = _closeModal(context),
+      activeCycleType = ref(0), // 当前选中的图表周期
+      tradedList = ref<QueryHistoryTikDatasRsp[]>([]);
 
-        function watchMore() {
-            context.emit('update', ComponentType.marketContent);
-        }
+    function watchMore() {
+      context.emit('update', ComponentType.marketContent);
+    }
 
-        // 周期类型
-        const chartType = [
-            { label: '分时', name: 'time', value: CycleType.time },
-            { label: '1分钟', name: 'minutes', value: CycleType.minutes },
-            { label: '5分钟', name: 'minutes5', value: CycleType.minutes5 },
-            { label: '30分钟', name: 'minutes30', value: CycleType.minutes30 },
-            { label: '60分钟', name: 'minutes60', value: CycleType.minutes60 },
-            { label: '4小时', name: 'Hours4', value: CycleType.Hours4 },
-            { label: '日 K', name: 'days', value: CycleType.days },
-        ];
+    // 周期类型
+    const chartType = [
+      { code: CycleType.time, label: '分时' },
+      { code: CycleType.minutes, label: '1分钟' },
+      { code: CycleType.minutes5, label: '5分钟' },
+      { code: CycleType.minutes30, label: '30分钟' },
+      { code: CycleType.minutes60, label: '60分钟' },
+      { code: CycleType.Hours4, label: '4小时' },
+      { code: CycleType.days, label: '日K' },
+    ];
 
-        // 切换图表周期类型
-        const changeCycleType = (e: { key: string }) => {
-            activeCycleType.value = chartType.find((item) => item.name === e.key)!.value;
-        };
+    // 切换图表周期类型
+    const changeCycleType = (index: number) => {
+      activeCycleType.value = index;
+    };
 
-        // Tik列表
-        const getHistoryTikDatas = (startTime: string, endTime: string) => {
-            const param = {
-                goodsCode: props.selectedRow.goodscode.toUpperCase(),
-                count: 20,
-                startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
-                endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
-            };
-            QueryHistoryTikDatas(param).then((res) => {
-                tradedList.value = res;
-            });
-        };
+    // Tik列表
+    const getHistoryTikDatas = (startTime: string, endTime: string) => {
+      const param = {
+        goodsCode: props.selectedRow.goodscode.toUpperCase(),
+        count: 20,
+        startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
+        endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
+      };
+      QueryHistoryTikDatas(param).then((res) => {
+        tradedList.value = res;
+      });
+    };
 
-        // 监听行情变化刷Tik列表
-        watch(
-            () => props.selectedRow.last,
-            () => {
-                if (tradedList.value.length > 19) {
-                    // 移除列表最后一条记录
-                    tradedList.value.pop();
-                }
-                // 向列表开头添加新纪录
-                tradedList.value.unshift({
-                    AV: 0,
-                    Ask: 0,
-                    BV: 0,
-                    Bid: 0,
-                    HI: 0,
-                    HV: 0,
-                    PE: props.selectedRow.last,
-                    TDR: 0,
-                    TK: 0,
-                    TS: props.selectedRow.lasttime,
-                    TT: 0,
-                    Vol: props.selectedRow.lastvolume,
-                });
-            }
-        );
+    // 监听行情变化刷Tik列表
+    watch(
+      () => props.selectedRow.last,
+      () => {
+        if (tradedList.value.length > 19) {
+          // 移除列表最后一条记录
+          tradedList.value.pop();
+        }
+        // 向列表开头添加新纪录
+        tradedList.value.unshift({
+          AV: 0,
+          Ask: 0,
+          BV: 0,
+          Bid: 0,
+          HI: 0,
+          HV: 0,
+          PE: props.selectedRow.last,
+          TDR: 0,
+          TK: 0,
+          TS: props.selectedRow.lasttime,
+          TT: 0,
+          Vol: props.selectedRow.lastvolume,
+        });
+      }
+    );
 
-        return {
-            cancel,
-            visible,
-            chartType,
-            tradedList,
-            CycleType,
-            activeCycleName,
-            activeCycleType,
-            activeSeriesType,
-            changeUnit,
-            watchMore,
-            formatTime,
-            quoteChange,
-            handleNoneValue,
-            quoteAmplitude,
-            handleQuotePriceColor,
-            changeCycleType,
-            getHistoryTikDatas,
-        };
-    },
+    return {
+      cancel,
+      visible,
+      chartType,
+      tradedList,
+      CycleType,
+      activeCycleType,
+      changeUnit,
+      watchMore,
+      formatTime,
+      quoteChange,
+      handleNoneValue,
+      quoteAmplitude,
+      handleQuotePriceColor,
+      changeCycleType,
+      getHistoryTikDatas,
+    };
+  },
 });
 </script>
 

+ 3 - 1
src/views/order/futures_information/components/futures_information_entrust/columns.tsx

@@ -45,7 +45,9 @@ export function getColumns() {
         {
             title: '委托价',
             key: 'orderprice',
-            customRender: ({ text }: { text: number }) => text.toFixed(2)
+            customRender: ({ record }: { record: QueryErmcpOrderDetailsRsp }) => {
+                return record.orderprice.toFixed(record.decimalplace)
+            }
         },
         {
             title: '冻结保证金',

+ 3 - 1
src/views/order/futures_information/components/futures_information_success/columns.tsx

@@ -33,7 +33,9 @@ export function getColumns() {
         {
             title: '成交价格',
             key: 'tradeprice',
-            customRender: ({ text }: { text: number }) => text.toFixed(2)
+            customRender: ({ record }: { record: QueryErmcpTradeDetailsRsp }) => {
+                return record.tradeprice.toFixed(record.decimalplace)
+            }
         },
         {
             title: '手续费',

+ 0 - 0
src/common/components/echarts/echart-base/index.less → src/views/report/future_report/list/future_report/index.less