li.shaoyi 3 years ago
parent
commit
593d25f52a
32 changed files with 1566 additions and 1114 deletions
  1. 28 0
      src/@next/components/base/echarts/core.ts
  2. 2 24
      src/@next/components/base/echarts/index.less
  3. 46 0
      src/@next/components/base/echarts/index.vue
  4. 68 0
      src/@next/components/base/echarts/setup.ts
  5. 8 6
      src/@next/components/modules/echarts-kline/index.less
  6. 141 0
      src/@next/components/modules/echarts-kline/index.vue
  7. 6 4
      src/@next/components/modules/echarts-timeline/index.less
  8. 51 0
      src/@next/components/modules/echarts-timeline/index.vue
  9. 41 0
      src/@next/constants/enum/chart.ts
  10. 80 82
      src/@next/hooks/echarts/candlestick/dataset.ts
  11. 215 0
      src/@next/hooks/echarts/candlestick/index.ts
  12. 38 37
      src/@next/hooks/echarts/candlestick/interface.ts
  13. 81 99
      src/@next/hooks/echarts/candlestick/options.ts
  14. 52 34
      src/@next/hooks/echarts/timeline/dataset.ts
  15. 108 0
      src/@next/hooks/echarts/timeline/index.ts
  16. 49 0
      src/@next/hooks/echarts/timeline/interface.ts
  17. 98 102
      src/@next/hooks/echarts/timeline/options.ts
  18. 0 59
      src/common/components/echarts/echarts-base/index.vue
  19. 0 103
      src/common/components/echarts/echarts-base/setup.ts
  20. 0 311
      src/common/components/echarts/echarts-kline/index.vue
  21. 0 131
      src/common/components/echarts/echarts-timeline/index.vue
  22. 0 40
      src/common/components/echarts/echarts-timeline/type.ts
  23. 17 31
      src/common/components/echarts/index.vue
  24. 1 1
      src/views/hedgeditem/exposure/list/index.vue
  25. 143 0
      src/views/hedgeditem/futures/in/components/add/form.ts
  26. 135 0
      src/views/hedgeditem/futures/in/components/add/index.vue
  27. 12 4
      src/views/hedgeditem/futures/in/index.vue
  28. 3 2
      src/views/hedgeditem/spot/contract/index.vue
  29. 1 1
      src/views/hedgeditem/spot/plan/components/bind/index.vue
  30. 3 2
      src/views/hedgeditem/spot/plan/index.vue
  31. 73 7
      src/views/hedgeditem/trade/running/index.vue
  32. 66 34
      src/views/information/spot-contract/components/modify/index.vue

+ 28 - 0
src/@next/components/base/echarts/core.ts

@@ -0,0 +1,28 @@
+/**
+ * 按需引入https://echarts.apache.org/zh/tutorial.html#%E5%9C%A8%E6%89%93%E5%8C%85%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8%20ECharts
+ */
+import * as echarts from 'echarts/core'
+import { CandlestickChart, CandlestickSeriesOption, BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts'
+import { MarkLineComponent, DatasetComponent, DataZoomComponent, GridComponent, GridComponentOption } from 'echarts/components'
+import { CanvasRenderer } from 'echarts/renderers'
+
+echarts.use([
+    LineChart,
+    MarkLineComponent,
+    DataZoomComponent,
+    DatasetComponent,
+    GridComponent,
+    BarChart,
+    CandlestickChart,
+    CanvasRenderer
+])
+
+type ECOption = echarts.ComposeOption<CandlestickSeriesOption | BarSeriesOption | LineSeriesOption | GridComponentOption>;
+
+export {
+    echarts,
+}
+
+export type {
+    ECOption,
+}

+ 2 - 24
src/common/components/echarts/echarts-base/index.less → src/@next/components/base/echarts/index.less

@@ -1,27 +1,5 @@
-.mtp-echarts {
-    flex  : 1;
-    margin: auto;
-
-    &.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;
-        }
-    }
+.app-echarts {
+    flex: 1;
 
     .tooltip {
         &-title {

+ 46 - 0
src/@next/components/base/echarts/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <div ref="chartElement" class="app-echarts" :style="{ width: width, height: height }"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType, watch } from 'vue'
+import { ECOption } from './core'
+import { useEcharts } from './setup'
+
+export default defineComponent({
+  props: {
+    // 图表宽度
+    width: {
+      type: String,
+      default: '100%',
+    },
+    // 图表高度
+    height: {
+      type: String,
+      default: '100%',
+    },
+    // 图表配置项
+    option: {
+      type: Object as PropType<ECOption>,
+      default: () => ({}),
+    },
+    dataIndex: {
+      type: Number,
+      default: 0,
+    },
+  },
+  setup(props) {
+    const { chartElement, setOptions } = useEcharts();
+
+    watch(() => props.option, (val) => setOptions(val));
+
+    return {
+      chartElement,
+    }
+  }
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 68 - 0
src/@next/components/base/echarts/setup.ts

@@ -0,0 +1,68 @@
+import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue'
+import { debounce } from '@/utils/time'
+import { echarts, ECOption } from './core'
+import ResizeObserver from 'resize-observer-polyfill'
+
+// 默认配置项
+const defaultOption: ECOption = {
+    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: ECOption, notMerge = false) => {
+        chart?.setOption(option, notMerge);
+    }
+
+    // 图表初始化
+    onMounted(() => {
+        const el = chartElement.value;
+        if (el) {
+            // 初始化图表
+            chart = echarts.init(el);
+            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];
+                    context?.emit('update:dataIndex', dataIndex);
+                }
+            })
+
+            // 图表重置大小
+            const resize = debounce(() => {
+                chart.resize && chart.resize();
+            }, 50);
+
+            // 监听元素变化
+            new ResizeObserver(resize).observe(el);
+            context?.emit('ready', chart);
+        }
+    })
+
+    onUnmounted(() => {
+        chart?.dispose();
+    })
+
+    return {
+        chartElement,
+        setOptions
+    }
+}

+ 8 - 6
src/common/components/echarts/echarts-kline/index.less → src/@next/components/modules/echarts-kline/index.less

@@ -1,13 +1,17 @@
-.mtp-echats-kline {
+.app-echats-kline {
     display       : flex;
     flex-direction: column;
     height        : 100%;
 
+    &__tip {
+        margin: auto;
+    }
+
     &__container {
         display       : flex;
         flex-direction: column;
 
-        .mtp-echarts {
+        .app-echarts {
             flex: 1;
         }
 
@@ -16,12 +20,10 @@
             color          : #7a8a94;
             font-size      : 12px;
             list-style-type: none;
-            padding        : 0 4%;
+            padding        : 4px 4%;
             margin         : 0;
 
             &-item {
-                white-space: nowrap;
-
                 &:not(:first-child) {
                     margin-left: 10px;
                 }
@@ -54,6 +56,6 @@
     }
 
     .mtp-tabbar {
-        padding: 0 4%;
+        padding: 4px 4%;
     }
 }

+ 141 - 0
src/@next/components/modules/echarts-kline/index.vue

@@ -0,0 +1,141 @@
+<template>
+  <div class="app-echats-kline">
+    <template v-if="loading">
+      <div class="app-echats-kline__tip">正在加载...</div>
+    </template>
+    <template v-else-if="isEmpty">
+      <div class="app-echats-kline__tip">暂无数据</div>
+    </template>
+    <template v-else>
+      <div class="app-echats-kline__container main">
+        <ul class="legend">
+          <li class="legend-item">开: {{ selectedItem.open }}</li>
+          <li class="legend-item">收: {{ selectedItem.close }}</li>
+          <li class="legend-item">高: {{ selectedItem.highest }}</li>
+          <li class="legend-item">低: {{ selectedItem.lowest }}</li>
+          <li class="legend-item">MA5: {{ selectedItem.ma5 }}</li>
+          <li class="legend-item">MA10: {{ selectedItem.ma10 }}</li>
+          <li class="legend-item">MA15: {{ selectedItem.ma15 }}</li>
+        </ul>
+        <app-echarts :option="options.candlestick" v-model:dataIndex="dataIndex" @ready="mainReady" />
+      </div>
+      <template v-if="showIndicator">
+        <div class="app-echats-kline__container indicator">
+          <!-- MACD -->
+          <section class="section" v-if="activeSeriesType === ChartSeriesType.MACD">
+            <ul class="legend">
+              <li class="legend-item">MACD: {{ selectedItem.macd }}</li>
+              <li class="legend-item">DIF: {{ selectedItem.dif }}</li>
+              <li class="legend-item">DEA: {{ selectedItem.dea }}</li>
+            </ul>
+            <app-echarts :option="options.macd" v-model:dataIndex="dataIndex" @ready="indicatorReady" />
+          </section>
+          <!-- VOL -->
+          <section class="section" v-if="activeSeriesType === ChartSeriesType.VOL">
+            <ul class="legend">
+              <li class="legend-item">VOL: {{ selectedItem.vol }}</li>
+            </ul>
+            <app-echarts :option="options.vol" v-model:dataIndex="dataIndex" @ready="indicatorReady" />
+          </section>
+          <!-- KDJ -->
+          <section class="section" v-if="activeSeriesType === ChartSeriesType.KDJ">
+            <ul class="legend">
+              <li class="legend-item">K: {{ selectedItem.k }}</li>
+              <li class="legend-item">D: {{ selectedItem.d }}</li>
+              <li class="legend-item">J: {{ selectedItem.j }}</li>
+            </ul>
+            <app-echarts :option="options.kdj" v-model:dataIndex="dataIndex" @ready="indicatorReady" />
+          </section>
+          <!-- CCI -->
+          <section class="section" v-if="activeSeriesType === ChartSeriesType.CCI">
+            <ul class="legend">
+              <li class="legend-item">CCI: {{ selectedItem.cci }}</li>
+            </ul>
+            <app-echarts :option="options.cci" v-model:dataIndex="dataIndex" @ready="indicatorReady" />
+          </section>
+        </div>
+        <mtp-tabbar theme="menu" :data="chartSeriesTypeList" @change="tabChange" />
+      </template>
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, PropType, watch } from 'vue'
+import { echarts } from '@/@next/components/base/echarts/core'
+import { ChartCycleType, ChartSeriesType, chartSeriesTypeList } from '@/@next/constants/enum/chart'
+import { useCandlestickChart } from '@/@next/hooks/echarts/candlestick'
+import AppEcharts from '@/@next/components/base/echarts/index.vue'
+import MtpTabbar from '@/common/components/tabbar/index.vue'
+
+export default defineComponent({
+  components: {
+    AppEcharts,
+    MtpTabbar,
+  },
+  props: {
+    goodscode: {
+      type: String,
+      required: true,
+    },
+    // 周期类型
+    cycleType: {
+      type: Number as PropType<ChartCycleType>,
+      default: ChartCycleType.minutes,
+    },
+    // 是否显示指标
+    showIndicator: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  setup(props) {
+    const { loading, dataIndex, isEmpty, options, selectedItem, initData, initOptions } = useCandlestickChart(props.goodscode);
+    const activeSeriesType = ref(ChartSeriesType.MACD); // 当前选中的指标
+    const chartGroup = new Map<string, echarts.ECharts>(); // 图表联动实例组
+
+    // 指标切换
+    const tabChange = (item: { label: string; value: number; }) => {
+      activeSeriesType.value = item.value;
+      setTimeout(() => {
+        initOptions();
+      }, 0);
+    }
+
+    const mainReady = (chart: echarts.ECharts) => {
+      chartGroup.set('main', chart);
+      initOptions();
+    }
+
+    const indicatorReady = (chart: echarts.ECharts) => {
+      chartGroup.set('indicator', chart);
+      echarts.connect([...chartGroup.values()]); // 图表联动
+    }
+
+    // 监听周期选择变化
+    watch(() => props.cycleType, (val) => {
+      initData(val);
+    }, {
+      immediate: true
+    })
+
+    return {
+      loading,
+      dataIndex,
+      activeSeriesType,
+      ChartSeriesType,
+      isEmpty,
+      options,
+      chartSeriesTypeList,
+      selectedItem,
+      tabChange,
+      mainReady,
+      indicatorReady
+    }
+  }
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 6 - 4
src/common/components/echarts/echarts-timeline/index.less → src/@next/components/modules/echarts-timeline/index.less

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

+ 51 - 0
src/@next/components/modules/echarts-timeline/index.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="app-echats-timeline">
+    <template v-if="loading">
+      <div class="app-echats-timeline__tip">正在加载...</div>
+    </template>
+    <template v-else-if="isEmpty">
+      <div class="app-echats-timeline__tip">暂无数据</div>
+    </template>
+    <template v-else>
+      <ul class="legend">
+        <li class="legend-item">收: {{ selectedItem.close }}</li>
+        <li class="legend-item">MA5: {{ selectedItem.ma5 }}</li>
+      </ul>
+      <app-echarts :option="options.timeline" v-model:dataIndex="dataIndex" @ready="initOptions" />
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import { useTimelineChart } from '@/@next/hooks/echarts/timeline'
+import AppEcharts from '@/@next/components/base/echarts/index.vue'
+
+export default defineComponent({
+  components: {
+    AppEcharts
+  },
+  props: {
+    goodscode: {
+      type: String,
+      required: true,
+    },
+  },
+  setup(props, context) {
+    const { loading, dataIndex, isEmpty, options, selectedItem, initOptions } = useTimelineChart(context, props.goodscode);
+
+    return {
+      loading,
+      dataIndex,
+      isEmpty,
+      options,
+      selectedItem,
+      initOptions
+    }
+  }
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 41 - 0
src/@next/constants/enum/chart.ts

@@ -0,0 +1,41 @@
+/**
+ * 图表周期类型
+ */
+export enum ChartCycleType {
+    time = -1, // 分时
+    minutes = 1, // 1分钟
+    minutes5 = 2, // 5分钟
+    minutes30 = 3, // 30分钟
+    minutes60 = 4, // 60分钟
+    hours2 = 120, // 2小时
+    Hours4 = 240, // 4小时
+    days = 11, // 日线
+}
+
+/**
+ * 图表指标类型
+ */
+export enum ChartSeriesType {
+    MACD,
+    VOL,
+    KDJ,
+    CCI,
+}
+
+export const chartCycleTypeList: { label: string; value: number; }[] = [
+    { label: '分时', value: ChartCycleType.time },
+    { label: '1分钟', value: ChartCycleType.minutes },
+    { label: '5分钟', value: ChartCycleType.minutes5 },
+    { label: '30分钟', value: ChartCycleType.minutes30 },
+    { label: '60分钟', value: ChartCycleType.minutes60 },
+    { label: '2小时', value: ChartCycleType.hours2 },
+    { label: '4小时', value: ChartCycleType.Hours4 },
+    { label: '日线', value: ChartCycleType.days },
+]
+
+export const chartSeriesTypeList: { label: string; value: number; }[] = [
+    { label: 'MACD', value: ChartSeriesType.MACD },
+    { label: 'VOL', value: ChartSeriesType.VOL },
+    { label: 'KDJ', value: ChartSeriesType.KDJ },
+    { label: 'CCI', value: ChartSeriesType.CCI },
+]

+ 80 - 82
src/common/components/echarts/echarts-kline/dataset.ts → src/@next/hooks/echarts/candlestick/dataset.ts

@@ -1,51 +1,52 @@
-import { QueryHistoryDatasRsp } from '@/services/go/quote/interface';
-import { Dataset, Candlestick, MACD, VOL, KDJ, CCI } from './type'
-import moment from 'moment';
+import { reactive } from 'vue'
+import { QueryHistoryDatasRsp } from '@/services/go/quote/interface'
+import { EchartsDataset, Candlestick, MACD } from './interface'
+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 dataset = reactive<EchartsDataset>({
+        invalid: [],
+        candlestick: {
+            dimensions: ['date', 'open', 'close', 'lowest', 'highest', 'ma5', 'ma10', 'ma15'],
+            source: [],
+        },
+        macd: {
+            dimensions: ['date', 'macd', 'dif', 'dea'],
+            source: [],
+        },
+        vol: {
+            dimensions: ['date', 'vol'],
+            source: [],
+        },
+        kdj: {
+            dimensions: ['date', 'k', 'd', 'j'],
+            source: [],
+        },
+        cci: {
+            dimensions: ['date', 'cci'],
+            source: [],
+        }
+    })
+
+    // 清空数据
+    const clearData = () => {
+        dataset.invalid = [];
+        dataset.candlestick.source = [];
+        dataset.macd.source = [];
+        dataset.vol.source = [];
+        dataset.kdj.source = [];
+        dataset.cci.source = [];
     }
 
     // 处理行情数据
     const handleData = (rawData: QueryHistoryDatasRsp[], onReady?: () => void) => {
-        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); // 添加补充数据的索引位置
+            if (f) dataset.invalid.push(i); // 添加补充数据的索引位置
 
-            klineData.source.push({
+            dataset.candlestick.source.push({
                 date,
                 open: o,
                 close: c,
@@ -56,7 +57,7 @@ export function useDataset() {
                 ma15: '-',
             })
 
-            macdData.source.push({
+            dataset.macd.source.push({
                 date,
                 ema12: 0,
                 ema26: 0,
@@ -65,19 +66,19 @@ export function useDataset() {
                 macd: 0,
             })
 
-            volData.source.push({
+            dataset.vol.source.push({
                 date,
                 vol: tv,
             })
 
-            kdjData.source.push({
+            dataset.kdj.source.push({
                 date,
                 k: '-',
                 d: '-',
                 j: '-'
             })
 
-            cciData.source.push({
+            dataset.cci.source.push({
                 date,
                 cci: '-',
             })
@@ -88,23 +89,23 @@ export function useDataset() {
     }
 
     // 计算MA
-    const calcMA = (key: keyof Candlestick, count: number, startIndex = 0) => {
-        type T = Candlestick[keyof Candlestick];
-        const klineSource = klineData.source;
+    const calcMA = (count: 5 | 10 | 15, startIndex = 0) => {
+        const key: keyof Candlestick = count === 5 ? 'ma5' : count === 10 ? 'ma10' : 'ma15';
+        const candlestickSource = dataset.candlestick.source;
 
-        for (let i = startIndex; i < klineSource.length; i++) {
+        for (let i = startIndex; i < candlestickSource.length; i++) {
             // 判断是否补充数据
-            if (invalidData.includes(i)) {
-                const value = klineSource.length > 0 ? klineSource[i - 1][key].toString() : '-'; // 如果存在,取上一条记录的MA值
-                (<T>klineSource[i][key]) = value;
+            if (dataset.invalid.includes(i)) {
+                const value = i > 0 ? candlestickSource[i - 1][key] : '-'; // 如果存在,取上一条记录的MA值
+                candlestickSource[i][key] = value;
             } else {
                 let n = 0;
                 const tmpList: Candlestick[] = [];
 
                 for (let j = i; j >= 0; j--) {
-                    if (invalidData.includes(j)) continue; // 如果是补充数据,跳过本次循环
+                    if (dataset.invalid.includes(j)) continue; // 如果是补充数据,跳过本次循环
                     if (n === count) break; // 如果 n 等于计数,结束循环
-                    tmpList.push(klineSource[j]);
+                    tmpList.push(candlestickSource[j]);
                     n++;
                 }
 
@@ -112,31 +113,31 @@ export function useDataset() {
                     // 计算总价(收盘价总和)
                     const total = tmpList.reduce((res, e) => res + e.close, 0);
                     // 计算均价
-                    (<T>klineSource[i][key]) = (total / count).toFixed(2);
+                    candlestickSource[i][key] = (total / count).toFixed(2);
                 } else {
-                    (<T>klineSource[i][key]) = '-';
+                    candlestickSource[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 calcEMA = (count: 12 | 26, startIndex = 0) => {
+        const key: keyof MACD = count === 12 ? 'ema12' : 'ema26';
+        const macdSource = dataset.macd.source;
+        const candlestickSource = dataset.candlestick.source;
         const a = 2 / (count + 1); // 平滑系数
 
-        for (let i = startIndex; i < klineSource.length; i++) {
-            const close = klineSource[i].close; // 收盘价
+        for (let i = startIndex; i < candlestickSource.length; i++) {
+            const close = candlestickSource[i].close; // 收盘价
 
             if (i === 0) {
-                (<T>macdSource[i][key]) = close; // 初始EMA取收盘价
+                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;
+                macdSource[i][key] = value;
             }
         }
     }
@@ -144,10 +145,10 @@ export function useDataset() {
     // 计算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)
+        calcEMA(12, startIndex); // EMA(12) = 2 ÷ 13 * 今日收盘价(12) + 11 ÷ 13 * 昨日EMA(12)
+        calcEMA(26, startIndex);
 
-        const macdSource = macdData.source;
+        const macdSource = dataset.macd.source;
 
         for (let i = startIndex; i < macdSource.length; i++) {
             const { ema12, ema26 } = macdSource[i];
@@ -166,21 +167,21 @@ export function useDataset() {
     // 计算KDJ
     const calcKDJ = (startIndex = 0) => {
         const count = 9; // 以9日周期计算
-        const klineSource = klineData.source;
-        const kdjSource = kdjData.source;
+        const candlestickSource = dataset.candlestick.source;
+        const kdjSource = dataset.kdj.source;
 
-        for (let i = startIndex; i < klineSource.length; i++) {
+        for (let i = startIndex; i < candlestickSource.length; i++) {
             let n = 0;
             const tmpList: Candlestick[] = [];
 
             for (let j = i; j >= 0; j--) {
                 if (n === count) break; // 如果 n 等于计数,结束循环
-                tmpList.push(klineSource[j]);
+                tmpList.push(candlestickSource[j]);
                 n++;
             }
 
             if (n === count) {
-                const close = klineSource[i].close, // 收盘价
+                const close = candlestickSource[i].close, // 收盘价
                     n9 = tmpList.map((item) => item.close),
                     h9 = Math.max(...n9), // 9天内最高价
                     l9 = Math.min(...n9); // 9天内最低价
@@ -206,22 +207,22 @@ export function useDataset() {
     // 计算CCI
     const calcCCI = (startIndex = 0) => {
         const count = 14; // 以14日周期计算
-        const klineSource = klineData.source;
-        const cciSource = cciData.source;
+        const candlestickSource = dataset.candlestick.source;
+        const cciSource = dataset.cci.source;
 
-        for (let i = startIndex; i < klineSource.length; i++) {
+        for (let i = startIndex; i < candlestickSource.length; i++) {
             let n = 0;
             const tmpList: Candlestick[] = [];
 
             for (let j = i; j >= 0; j--) {
                 if (n === count) break; // 如果 n 等于计数,结束循环
-                tmpList.push(klineSource[j]);
+                tmpList.push(candlestickSource[j]);
                 n++;
             }
 
             if (n === count) {
                 const calcTP = (item: Candlestick) => (item.highest + item.lowest + item.close) / 3; // TP = (最高价 + 最低价 + 收盘价) ÷ 3
-                const tp = calcTP(klineSource[i]),
+                const tp = calcTP(candlestickSource[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
 
@@ -231,23 +232,20 @@ export function useDataset() {
         }
     }
 
-    // 计算各指标
+    // 计算各指标
     const calcIndicator = (startIndex = 0) => {
-        calcMA('ma5', 5, startIndex);
-        calcMA('ma10', 10, startIndex);
-        calcMA('ma15', 15, startIndex);
+        calcMA(5, startIndex);
+        calcMA(10, startIndex);
+        calcMA(15, startIndex);
         calcMACD(startIndex);
         calcKDJ(startIndex);
         calcCCI(startIndex);
     }
 
     return {
-        klineData,
-        macdData,
-        volData,
-        kdjData,
-        cciData,
+        dataset,
         handleData,
+        clearData,
         calcIndicator,
     }
 }

+ 215 - 0
src/@next/hooks/echarts/candlestick/index.ts

@@ -0,0 +1,215 @@
+import { ref, computed, watch } from 'vue'
+//import { timerInterceptor } from '@/utils/timer'
+import { ChartCycleType } from '@/@next/constants/enum/chart'
+import { QueryHistoryDatas, QueryQuoteDayRsp } from '@/services/go/quote/interface'
+import { QueryHistoryDatas as queryHistoryDatas } from '@/services/go/quote'
+import { getQuoteDayInfoByCode } from '@/services/bus/goods'
+import { useDataset } from './dataset'
+import { useOptions } from './options'
+import moment from 'moment'
+
+export function useCandlestickChart(goodscode: string) {
+    const { dataset, handleData, clearData, calcIndicator } = useDataset();
+    const { options, initOptions, updateOptions } = useOptions(dataset);
+
+    const loading = ref(false);
+    const isEmpty = ref(true);
+    const dataIndex = ref(-1); // 当前数据索引值
+    const cycleType = ref(ChartCycleType.minutes);
+    const quote = ref<QueryQuoteDayRsp>(getQuoteDayInfoByCode(goodscode)!); // 商品实时行情
+
+    // 当前选中的数据项
+    const selectedItem = computed(() => {
+        let result: { [key: string]: number | string } = {
+            open: '--',
+            close: '--',
+            highest: '--',
+            lowest: '--',
+            ma5: '--',
+            ma10: '--',
+            ma15: '--',
+            macd: '--',
+            dif: '--',
+            dea: '--',
+            vol: '--',
+            k: '--',
+            d: '--',
+            j: '--',
+            cci: '--'
+        }
+
+        const data = dataset.candlestick.source[dataIndex.value];
+        if (data) {
+            const { open, close, highest, lowest, ma5, ma10, ma15 } = data;
+            const { macd, dif, dea } = dataset.macd.source[dataIndex.value];
+            const { vol } = dataset.vol.source[dataIndex.value];
+            const { k, d, j } = dataset.kdj.source[dataIndex.value];
+            const { cci } = dataset.cci.source[dataIndex.value];
+
+            result = { open, close, highest, lowest, ma5, ma10, ma15, macd, dif, dea, vol, k, d, j, cci }
+        }
+
+        return result;
+    })
+
+    /**
+     * 初始化数据
+     * @param cycletype 周期类型
+     */
+    const initData = (cycletype: ChartCycleType) => {
+        clearData();
+        dataIndex.value = -1;
+        cycleType.value = cycletype;
+        loading.value = true;
+        isEmpty.value = true;
+
+        const params: QueryHistoryDatas = {
+            cycleType: cycletype as any,
+            goodsCode: goodscode.toUpperCase(),
+            count: 1440,
+        }
+
+        // 获取历史行情
+        queryHistoryDatas(params).then((res) => {
+            if (res.length) {
+                // 日期升序排序
+                const data = res.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf());
+                dataIndex.value = res.length - 1;
+                isEmpty.value = false;
+                handleData(data);
+            }
+        }).finally(() => {
+            loading.value = false;
+        })
+    }
+
+    /**
+     * 获取周期毫秒数
+     * @returns 
+     */
+    const getCycleMilliseconds = () => {
+        const milliseconds = 60 * 1000; // 一分钟毫秒数
+        switch (cycleType.value) {
+            case ChartCycleType.minutes5: {
+                return milliseconds * 5;
+            }
+            case ChartCycleType.minutes30: {
+                return milliseconds * 30;
+            }
+            case ChartCycleType.minutes60: {
+                return milliseconds * 60;
+            }
+            case ChartCycleType.hours2: {
+                return milliseconds * 2 * 60;
+            }
+            case ChartCycleType.Hours4: {
+                return milliseconds * 4 * 60;
+            }
+            case ChartCycleType.days: {
+                return milliseconds * 24 * 60;
+            }
+            default: {
+                return milliseconds;
+            }
+        }
+    }
+
+    /**
+     * 更新图表数据
+     */
+    const updateChart = () => {
+        if (quote.value) {
+            const { last, lasttime } = quote.value;
+            const { candlestick, macd, vol, kdj, cci } = dataset;
+            const lastIndex = candlestick.source.length - 1; // 历史数据最后索引位置
+            const cycleMilliseconds = getCycleMilliseconds();
+
+            const oldTime = lastIndex === -1 ? moment(lasttime) : moment(candlestick.source[lastIndex].date); // 历史行情最后时间
+            const diffTime = moment(lasttime).valueOf() - oldTime.valueOf(); // 计算时间差
+
+            if (diffTime > cycleMilliseconds * 2) {
+                // 时间间隔超过两个周期,重新请求历史数据
+                //timerInterceptor.debounce(() => initData(cycleType.value), 1000, 'updateChart');
+            } else {
+                // 判断时间差是否大于周期时间
+                if (lastIndex === -1 || diffTime > cycleMilliseconds) {
+                    oldTime.add(cycleMilliseconds, 'ms');
+                    const lastDate = oldTime.format('YYYY-MM-DD HH:mm:ss');
+
+                    // 新增K线数据
+                    candlestick.source.push({
+                        date: lastDate,
+                        open: last,
+                        close: last,
+                        lowest: last,
+                        highest: last,
+                        ma5: '-',
+                        ma10: '-',
+                        ma15: '-',
+                    })
+
+                    // 新增MACD数据
+                    macd.source.push({
+                        date: lastDate,
+                        ema12: 0,
+                        ema26: 0,
+                        dif: 0,
+                        dea: 0,
+                        macd: 0,
+                    })
+
+                    // 新增VOL数据
+                    vol.source.push({
+                        date: lastDate,
+                        vol: 0,
+                    })
+
+                    // 新增KDJ数据
+                    kdj.source.push({
+                        date: lastDate,
+                        k: '-',
+                        d: '-',
+                        j: '-',
+                    })
+
+                    // 新增CCI数据
+                    cci.source.push({
+                        date: lastDate,
+                        cci: '-',
+                    })
+                } else {
+                    // 更新列表中最后一条记录的数据
+                    const record = candlestick.source[lastIndex];
+                    if (record.lowest > last) {
+                        record.lowest = last; // 更新最低价
+                    }
+                    if (record.highest < last) {
+                        record.highest = last; // 更新最高价
+                    }
+                    record.close = last; // 更新收盘价
+                }
+
+                // 计算各项指标
+                calcIndicator(lastIndex === -1 ? 0 : lastIndex);
+                updateOptions();
+            }
+        }
+    }
+
+    // 监听行情推送
+    watch(() => quote.value?.last, () => {
+        if (!loading.value && !isEmpty.value) {
+            updateChart();
+        }
+    })
+
+    return {
+        loading,
+        isEmpty,
+        dataIndex,
+        options,
+        selectedItem,
+        initData,
+        initOptions,
+    }
+}

+ 38 - 37
src/common/components/echarts/echarts-kline/type.ts → src/@next/hooks/echarts/candlestick/interface.ts

@@ -1,33 +1,24 @@
-/**
- * 周期类型
- */
-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, // 日线
-}
+
+import { ECOption } from '@/@next/components/base/echarts/core'
 
 /**
- * 指标类型
+ * 图表数据集
  */
-export enum SeriesType {
-    MACD,
-    VOL,
-    KDJ,
-    CCI,
+export interface EchartsDataset {
+    invalid: number[]; // 行情历史数据中所有补充数据的索引位置(用于指标计算)
+    candlestick: Dataset<Candlestick>;
+    macd: Dataset<MACD>;
+    vol: Dataset<VOL>;
+    kdj: Dataset<KDJ>;
+    cci: Dataset<CCI>;
 }
 
 /**
- * 图表数据
+ * 图表数据
  */
-export type Dataset<T> = {
-    dimensions: (keyof T)[],
-    source: T[],
+export interface Dataset<T> {
+    dimensions: (keyof T)[];
+    source: T[];
 }
 
 /**
@@ -45,7 +36,7 @@ export type Candlestick = {
 }
 
 /**
- * MACD
+ * MACD指标
  */
 export type MACD = {
     date: string, // xAxis数据,必须是第一个属性
@@ -57,15 +48,15 @@ export type MACD = {
 }
 
 /**
- * VOL
+ * VOL指标
  */
 export type VOL = {
     date: string, // xAxis数据,必须是第一个属性
     vol: number,
 }
 
-/**
- * KDJ
+/** 
+ * KDJ指标
  */
 export type KDJ = {
     date: string, // xAxis数据,必须是第一个属性
@@ -74,8 +65,8 @@ export type KDJ = {
     j: string,
 }
 
-/**
- * CCI
+/** 
+ * CCI指标
  */
 export type CCI = {
     date: string, // xAxis数据,必须是第一个属性
@@ -83,13 +74,23 @@ export type CCI = {
 }
 
 /**
- * 图表主题色
+ * 图表配置项
+ */
+export interface EchartsOptions {
+    colors: Colors;
+    candlestick: ECOption;
+    macd: ECOption;
+    vol: ECOption;
+    kdj: ECOption;
+    cci: ECOption;
+}
+
+/**
+ * 图表颜色
  */
-export type Colors = {
-    upColor: string,
-    downColor: string,
-    xAxisLineColor: string,
-    yAxisLineColor: string,
-    seriesMarkLabelColor: string,
-    seriesMarkLineColor: string,
+export interface Colors {
+    upColor: string;
+    downColor: string;
+    xAxisLineColor: string;
+    yAxisLineColor: string;
 }

+ 81 - 99
src/common/components/echarts/echarts-kline/options.ts → src/@next/hooks/echarts/candlestick/options.ts

@@ -1,25 +1,54 @@
-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 { reactive, watch } from 'vue'
+import { ECOption } from '@/@next/components/base/echarts/core'
 import { throttle } from '@/utils/time'
-import moment from 'moment';
+import { getTheme } from '@/common/config/theme'
+import { EchartsDataset, EchartsOptions, Colors } from './interface'
+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 theme = getTheme();
+
+function getColors() {
+    // 默认主题色配置
+    const defaultColors: Colors = {
+        upColor: '#eb5454',
+        downColor: '#47b262',
+        xAxisLineColor: '#171b1d',
+        yAxisLineColor: '#171b1d',
+    }
+
+    const colors = {
+        default: defaultColors,
+        dark: defaultColors,
+        light: {
+            ...defaultColors,
+            xAxisLineColor: '#dae5ec',
+            yAxisLineColor: '#dae5ec',
+        }
+    }
+
+    return colors[theme.value];
+}
+
+export function useOptions(dataset: EchartsDataset) {
+    // 图表配置项
+    const options = reactive<EchartsOptions>({
+        colors: getColors(),
+        candlestick: {},
+        macd: {},
+        vol: {},
+        kdj: {},
+        cci: {},
+    })
+
+    const getDefaultOption = (): ECOption => {
+        const { source } = dataset.candlestick;
+        const { xAxisLineColor } = options.colors;
 
-    const getDefaultOption = (): EChartsOption => {
-        const { xAxisLineColor } = getColors();
         return {
             dataZoom: {
                 type: 'inside',
-                startValue: klineData.source.length - 120, // 起始显示K线条数(最新120条)
-                endValue: klineData.source.length,
+                startValue: source.length - 120, // 起始显示K线条数(最新120条)
+                endValue: source.length,
                 minValueSpan: 50, // 限制窗口缩放显示最少数据条数
                 maxValueSpan: 400, // 限制窗口缩放显示最大数据条数
             },
@@ -43,11 +72,9 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
     }
 
     // K线配置项
-    const setKlineOption = () => {
-        const { seriesMarkLineColor, seriesMarkLabelColor } = getColors();
-        const { dimensions, source } = klineData;
-
-        klineOption.value = {
+    const setCandlestickOption = () => {
+        const { dimensions, source } = dataset.candlestick;
+        options.candlestick = {
             ...getDefaultOption(),
             dataset: {
                 dimensions,
@@ -63,7 +90,7 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
                         symbol: 'none',
                         // 标线标签样式
                         label: {
-                            color: seriesMarkLabelColor,
+                            color: '#444',
                             fontWeight: 'bold',
                             backgroundColor: 'rgba(255,255,255,.75)',
                             padding: 5,
@@ -72,7 +99,7 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
                         // 标线样式
                         lineStyle: {
                             type: 'dashed',
-                            color: seriesMarkLineColor,
+                            color: 'rgba(255,255,255,.3)'
                         },
                         data: [
                             {
@@ -121,9 +148,9 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
 
     // MACD配置项
     const setMacdOption = () => {
-        const { upColor, downColor } = getColors();
-        const { dimensions, source } = macdData;
-        macdOption.value = {
+        const { upColor, downColor } = options.colors;
+        const { dimensions, source } = dataset.macd;
+        options.macd = {
             ...getDefaultOption(),
             dataset: {
                 dimensions,
@@ -136,8 +163,9 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
                     sampling: 'average',
                     barWidth: '20%',
                     itemStyle: {
-                        color: (params: any) => {
-                            if (params.data.macd > 0) {
+                        color: ({ data }) => {
+                            const { macd } = data as { macd: number };
+                            if (macd > 0) {
                                 return upColor;
                             } else {
                                 return downColor;
@@ -173,10 +201,8 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
 
     // VOL配置项
     const setVolOption = () => {
-        const { upColor, downColor } = getColors();
-        const { dimensions, source } = volData;
-
-        volOption.value = {
+        const { dimensions, source } = dataset.vol;
+        options.vol = {
             ...getDefaultOption(),
             dataset: {
                 dimensions,
@@ -188,17 +214,6 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
                     type: 'bar',
                     sampling: 'average',
                     barWidth: '60%',
-                    itemStyle: {
-                        color: (params: any) => {
-                            const { close, open } = klineData.source[params.dataIndex]
-                            // 判断收盘价是否高于或等于开盘价
-                            if (close >= open) {
-                                return upColor;
-                            } else {
-                                return downColor;
-                            }
-                        },
-                    }
                 },
             ],
         }
@@ -206,8 +221,8 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
 
     // KDJ配置项
     const setKdjOption = () => {
-        const { dimensions, source } = kdjData;
-        kdjOption.value = {
+        const { dimensions, source } = dataset.kdj;
+        options.kdj = {
             ...getDefaultOption(),
             dataset: {
                 dimensions,
@@ -253,8 +268,8 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
 
     // CCI配置项
     const setCciOption = () => {
-        const { dimensions, source } = cciData;
-        cciOption.value = {
+        const { dimensions, source } = dataset.cci;
+        options.cci = {
             ...getDefaultOption(),
             dataset: {
                 dimensions,
@@ -276,53 +291,21 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
         }
     }
 
-    // 获取图表主题色
-    const getColors = (): Colors => {
-        switch (theme.value) {
-            case ThemeEnum.default:
-            case ThemeEnum.dark: {
-                return {
-                    upColor: '#eb5454',
-                    downColor: '#47b262',
-                    xAxisLineColor: '#171B1D',
-                    yAxisLineColor: '#171B1D',
-                    seriesMarkLabelColor: '#3C454B',
-                    seriesMarkLineColor: '#666',
-                };
-            }
-            case ThemeEnum.light: {
-                return {
-                    upColor: '#eb5454',
-                    downColor: '#47b262',
-                    xAxisLineColor: '#DAE5EC',
-                    yAxisLineColor: '#DAE5EC',
-                    seriesMarkLabelColor: '#777',
-                    seriesMarkLineColor: '#ACB8C0',
-                };
-            }
-        }
-    }
-
-    const initOptions = (onReady?: () => void) => {
-        setKlineOption();
+    const initOptions = () => {
+        setCandlestickOption();
         setMacdOption();
         setVolOption();
         setKdjOption();
         setCciOption();
-        onReady && onReady();
     }
 
     // 动态更新数据
     const updateOptions = throttle(() => {
-        const klineSource = klineData.source;
-        const macdSource = macdData.source;
-        const volSource = volData.source;
-        const kdjSource = kdjData.source;
-        const cciSource = cciData.source;
+        const { candlestick, macd, vol, kdj, cci } = dataset;
 
-        klineOption.value = {
+        options.candlestick = {
             dataset: {
-                source: klineSource,
+                source: candlestick.source,
             },
             series: [
                 {
@@ -330,44 +313,43 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<MA
                     markLine: {
                         data: [
                             {
-                                yAxis: klineSource[klineSource.length - 1].close,
+                                yAxis: candlestick.source[candlestick.source.length - 1].close,
                             },
                         ],
                     },
                 },
             ],
         }
-        macdOption.value = {
+        options.macd = {
             dataset: {
-                source: macdSource,
+                source: macd.source,
             },
         }
-        volOption.value = {
+        options.vol = {
             dataset: {
-                source: volSource,
+                source: vol.source,
             },
         }
-        kdjOption.value = {
+        options.kdj = {
             dataset: {
-                source: kdjSource,
+                source: kdj.source,
             },
         }
-        cciOption.value = {
+        options.cci = {
             dataset: {
-                source: cciSource,
+                source: cci.source,
             },
         }
     }, 1000)
 
     // 监听主题变化
-    watch(theme, () => initOptions());
+    watch(theme, () => {
+        options.colors = getColors();
+        initOptions();
+    })
 
     return {
-        klineOption,
-        macdOption,
-        volOption,
-        kdjOption,
-        cciOption,
+        options,
         initOptions,
         updateOptions,
     }

+ 52 - 34
src/common/components/echarts/echarts-timeline/dataset.ts → src/@next/hooks/echarts/timeline/dataset.ts

@@ -1,19 +1,19 @@
-import { QueryTSDataRsp } from '@/services/go/quote/interface';
-import { State } from './type';
-import { getRangeTime } from '@/utils/time';
-import moment from 'moment';
+import { reactive } from 'vue'
+import { getRangeTime } from '@/utils/time'
+import { EchartsDataset } from './interface'
+import { QueryTSDataRsp } from '@/services/go/quote/interface'
+import moment from 'moment'
 
 export function useDataset() {
-    // 行情历史数据中所有补充数据的索引位置(用于指标计算)
-    const invalidData: number[] = [];
-    const state: State = {
+    const dataset = reactive<EchartsDataset>({
+        invalid: [],
         rawDate: [],
-        yestClose: 0,
+        yestclose: 0,
         decimal: 0,
         maxMark: 0,
         minMark: 0,
         interval: 0,
-        dataset: {
+        timeline: {
             dimensions: ['date', 'close', 'ma5'],
             source: {
                 date: [],
@@ -21,29 +21,46 @@ export function useDataset() {
                 ma5: []
             },
         }
+    })
+
+    // 清空数据
+    const clearData = () => {
+        dataset.invalid = [];
+        dataset.rawDate = [];
+        dataset.yestclose = 0;
+        dataset.decimal = 0;
+        dataset.maxMark = 0;
+        dataset.minMark = 0;
+        dataset.interval = 0;
+        dataset.timeline.source = {
+            date: [],
+            close: [],
+            ma5: []
+        };
     }
 
     // 处理行情数据
     const handleData = (rawData: QueryTSDataRsp, onReady?: () => void) => {
-        const { date, close, ma5 } = state.dataset.source;
-        invalidData.length = 0;
-        state.decimal = rawData.decimalPlace;
-        state.yestClose = rawData.preSettle;
+        const { date, close, ma5 } = dataset.timeline.source;
+        const { preSettle, decimalPlace, runSteps, historyDatas } = rawData;
+
+        dataset.decimal = decimalPlace;
+        dataset.yestclose = preSettle;
 
         // 开盘交易时间
-        for (let i = 0; i < rawData.runSteps.length; i++) {
-            const { start, end } = rawData.runSteps[i];
+        for (let i = 0; i < runSteps.length; i++) {
+            const { start, end } = 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');
+        for (let i = 0; i < historyDatas.length; i++) {
+            const { ts, c, f } = historyDatas[i];
+            const d = moment(ts).format('YYYY-MM-DD HH:mm:ss');
 
-            if (f) invalidData.push(i); // 添加补充数据的索引位置
+            if (f) dataset.invalid.push(i); // 添加补充数据的索引位置
 
-            state.rawDate.push(date);
+            dataset.rawDate.push(d);
             close.push(c);
             ma5.push('-');
         }
@@ -54,17 +71,17 @@ export function useDataset() {
 
     // 计算MA
     const calcMA = (startIndex = 0) => {
-        const { close, ma5 } = state.dataset.source;
+        const { close, ma5 } = dataset.timeline.source;
         for (let i = startIndex; i < close.length; i++) {
             // 判断是否补充数据
-            if (invalidData.includes(i)) {
+            if (dataset.invalid.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 (dataset.invalid.includes(j)) continue; // 如果是补充数据,跳过本次循环
                     if (n === 5) break; // 如果 n 等于计数,结束循环
                     tmpList.push(close[j]);
                     n++;
@@ -82,20 +99,20 @@ export function useDataset() {
         }
     }
 
-    // 计算图表最高低指标线
+    // 计算图表最高低指标线
     const calcMarkLine = () => {
-        const { close } = state.dataset.source;
-        const point = Math.pow(10, -state.decimal) * 10; // 图表上下保留10个报价点数
+        const { close } = dataset.timeline.source;
+        const point = Math.pow(10, -dataset.decimal) * 10; // 图表上下保留10个报价点数
         let max = Math.max(...close); // 取历史行情最高价
         let min = Math.min(...close); // 取历史行情最低价
 
         const last = close[close.length - 1], // 历史行情最后收盘价
-            a = state.yestClose - min, // 计算收盘价和最低价的差值
-            b = max - state.yestClose; // 计算收盘价和最高价的差值
+            a = dataset.yestclose - min, // 计算收盘价和最低价的差值
+            b = max - dataset.yestclose; // 计算收盘价和最高价的差值
 
         // 比较差值大小
         if (a > b) {
-            max = state.yestClose + a;
+            max = dataset.yestclose + a;
             if (last > max) {
                 const c = last - max;
                 max += c;
@@ -110,20 +127,21 @@ export function useDataset() {
             }
         }
 
-        state.maxMark = max + point;
-        state.minMark = min - point;
-        state.interval = (state.maxMark - state.minMark) / 6;
+        dataset.maxMark = max + point;
+        dataset.minMark = min - point;
+        dataset.interval = (dataset.maxMark - dataset.minMark) / 6;
     }
 
-    // 计算各指标
+    // 计算各指标
     const calcIndicator = (startIndex = 0) => {
         calcMA(startIndex);
         calcMarkLine();
     }
 
     return {
-        state,
+        dataset,
         handleData,
+        clearData,
         calcIndicator,
     }
 }

+ 108 - 0
src/@next/hooks/echarts/timeline/index.ts

@@ -0,0 +1,108 @@
+import { ref, computed, watch, SetupContext } from 'vue'
+//import { timerInterceptor } from '@/utils/timer'
+import { QueryTSData } from '@/services/go/quote'
+import { QueryQuoteDayRsp } from '@/services/go/quote/interface'
+import { useDataset } from './dataset'
+import { useOptions } from './options'
+import { getQuoteDayInfoByCode } from '@/services/bus/goods'
+import moment from 'moment'
+
+export function useTimelineChart(context: SetupContext, goodscode: string) {
+    const { dataset, handleData, clearData, calcIndicator } = useDataset();
+    const { options, initOptions, updateOptions } = useOptions(dataset);
+
+    const loading = ref(false);
+    const isEmpty = ref(false);
+    const dataIndex = ref(-1); // 当前数据索引值
+    const quote = ref<QueryQuoteDayRsp>(getQuoteDayInfoByCode(goodscode)!); // 商品实时行情
+
+    // 当前选中的数据项
+    const selectedItem = computed(() => {
+        const { close, ma5 } = dataset.timeline.source;
+        return {
+            close: close[dataIndex.value] ?? '--',
+            ma5: ma5[dataIndex.value] ?? '--'
+        }
+    })
+
+    /**
+     * 初始化数据
+     */
+    const initData = () => {
+        clearData();
+        dataIndex.value = -1;
+        isEmpty.value = true;
+        loading.value = true;
+
+        // 获取历史行情
+        QueryTSData(goodscode).then((res) => {
+            const { historyDatas, startTime, endTime } = res;
+            if (historyDatas.length) {
+                dataIndex.value = historyDatas.length - 1;
+                isEmpty.value = false;
+                handleData(res);
+            }
+
+            // 调用父级函数查询tik数据 (不合理的逻辑处理,待优化)
+            context.emit('ready', startTime, endTime);
+        }).finally(() => {
+            loading.value = false;
+        })
+    }
+
+    /**
+     * 更新图表数据
+     */
+    const updateChart = () => {
+        if (quote.value) {
+            const { last, lasttime } = quote.value;
+            const { close, ma5 } = dataset.timeline.source;
+            const lastIndex = close.length - 1; // 历史数据最后索引位置
+            const cycleMilliseconds = 60 * 1000; // 一分钟毫秒数
+
+            const oldTime = lastIndex === -1 ? moment(lasttime) : moment(dataset.rawDate[lastIndex]); // 历史行情最后时间
+            const diffTime = moment(lasttime).valueOf() - oldTime.valueOf(); // 计算时间差
+
+            if (diffTime > cycleMilliseconds * 2) {
+                // 时间间隔超过两个周期,重新请求历史数据
+                //timerInterceptor.debounce(() => initData(), 1000, 'updateChart');
+            } else {
+                // 判断时间差是否大于周期时间
+                if (lastIndex === -1 || diffTime > cycleMilliseconds) {
+                    oldTime.add(cycleMilliseconds, 'ms');
+                    const lastDate = oldTime.format('YYYY-MM-DD HH:mm:ss');
+
+                    // 新增分时数据
+                    dataset.rawDate.push(lastDate);
+                    close.push(last);
+                    ma5.push('-')
+                } else {
+                    close[lastIndex] = last; // 更新最后一条记录的收盘价
+                }
+
+                // 更新各种指标
+                calcIndicator(lastIndex === -1 ? 0 : lastIndex);
+                updateOptions();
+            }
+        }
+    }
+
+    // 监听行情推送
+    watch(() => quote.value?.last, () => {
+        if (!loading.value && !isEmpty.value) {
+            updateChart();
+        }
+    })
+
+    initData();
+
+    return {
+        loading,
+        isEmpty,
+        dataIndex,
+        options,
+        selectedItem,
+        initData,
+        initOptions,
+    }
+}

+ 49 - 0
src/@next/hooks/echarts/timeline/interface.ts

@@ -0,0 +1,49 @@
+import { echarts, ECOption } from '@/@next/components/base/echarts/core'
+
+/**
+ * 图表数据集
+ */
+export interface EchartsDataset {
+    invalid: number[]; // 行情历史数据中所有补充数据的索引位置(用于指标计算)
+    rawDate: string[]; // 原始日期
+    yestclose: number; // 昨日收盘价
+    decimal: number; // 小数位
+    maxMark: number; // Y轴最大刻度
+    minMark: number; // Y轴最小刻度
+    interval: number, // Y轴间隔高度
+    timeline: {
+        dimensions: (keyof Timeline)[];
+        source: Timeline;
+    }
+}
+
+/** 
+ * 分时线
+ */
+export type Timeline = {
+    date: string[], // xAxis数据,必须是第一个属性
+    close: number[],
+    ma5: string[],
+}
+
+/**
+ * 图表配置项
+ */
+export interface EchartsOptions {
+    colors: Colors;
+    timeline: ECOption;
+}
+
+/**
+ * 图表颜色
+ */
+export interface Colors {
+    upColor: string,
+    downColor: string,
+    xAxisLineColor: string,
+    yAxisLineColor: string,
+    seriesLineColor: string,
+    seriesMarkLabelColor: string,
+    seriesMarkLineColor: string,
+    seriesAreaGradients: echarts.LinearGradientObject,
+}

+ 98 - 102
src/common/components/echarts/echarts-timeline/options.ts → src/@next/hooks/echarts/timeline/options.ts

@@ -1,75 +1,81 @@
-import { ref, watch } from 'vue'
-import { formatDecimal } from '@/utils/number';
-import * as echarts from 'echarts';
-import { State, Colors } from './type'
-import { getTheme, ThemeEnum } from '@/common/config/theme';
+import { reactive, watch } from 'vue'
 import { throttle } from '@/utils/time'
+import { getTheme } from '@/common/config/theme'
+import { echarts } from '@/@next/components/base/echarts/core'
+import { EchartsDataset, EchartsOptions, Colors } from './interface'
 
-export function useOptions(state: State) {
-    const theme = getTheme(),
-        timeOption = ref<echarts.EChartsOption>({});
+const theme = getTheme();
 
-    // 获取图表主题色
-    const getColors = (): Colors => {
-        switch (theme.value) {
-            case ThemeEnum.default:
-            case ThemeEnum.dark: {
-                return {
-                    upColor: '#FF2B2B',
-                    downColor: '#1FF195',
-                    xAxisLineColor: '#171B1D',
-                    yAxisLineColor: '#171B1D',
-                    seriesLineColor: '#39afe6',
-                    seriesMarkLabelColor: '#3C454B',
-                    seriesMarkLineColor: '#666',
-                    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',
-                    seriesMarkLabelColor: '#777',
-                    seriesMarkLineColor: '#ACB8C0',
-                    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
-                    ),
-                };
-            }
+function getColors() {
+    // 默认主题色配置
+    const defaultColors: Colors = {
+        upColor: '#FF2B2B',
+        downColor: '#1FF195',
+        xAxisLineColor: '#171B1D',
+        yAxisLineColor: '#171B1D',
+        seriesLineColor: '#39afe6',
+        seriesMarkLabelColor: '#3C454B',
+        seriesMarkLineColor: '#666',
+        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 colors = {
+        default: defaultColors,
+        dark: defaultColors,
+        light: {
+            ...defaultColors,
+            downColor: '#00A843',
+            xAxisLineColor: '#DAE5EC',
+            yAxisLineColor: '#DAE5EC',
+            seriesLineColor: '#3864d7',
+            seriesMarkLabelColor: '#777',
+            seriesMarkLineColor: '#ACB8C0',
         }
     }
 
-    const initOptions = (onReady?: () => void) => {
-        const colors = getColors();
-        const { dataset, maxMark, minMark, interval, yestClose, decimal } = state;
+    return colors[theme.value];
+}
+
+export function useOptions(dataset: EchartsDataset) {
+    // 图表配置项
+    const options = reactive<EchartsOptions>({
+        colors: getColors(),
+        timeline: {},
+    })
 
-        timeOption.value = {
-            dataset,
+    // 计算涨跌幅百分比,涨跌幅=(今日收盘价-昨日收盘价)/昨日收盘价*100%
+    const calcRatio = (val: number) => {
+        const { yestclose } = dataset;
+        const num = Number(val);
+
+        if (isNaN(num)) {
+            return '-';
+        }
+        if (yestclose > 0) {
+            const result = (num - yestclose) / yestclose * 100;
+            return result.toFixed(2) + '%';
+        }
+        return '0%';
+    }
+
+    const initOptions = () => {
+        const { timeline, maxMark, minMark, interval, yestclose, decimal } = dataset;
+        const { colors } = options;
+
+        options.timeline = {
+            dataset: timeline,
             axisPointer: {
                 label: {
                     // 小数点精度
@@ -91,11 +97,13 @@ export function useOptions(state: State) {
                     min: minMark,
                     interval,
                     axisLabel: {
-                        formatter: (val: number) => formatDecimal(val, decimal).toString(),
-                        color: (val: any) => {
-                            const num = Number(val).toFixed(decimal);
-                            if (Number(num) > yestClose) return colors.upColor;
-                            if (Number(num) < yestClose) return colors.downColor;
+                        formatter: (val: number) => val.toFixed(decimal),
+                        color: (val) => {
+                            if (val) {
+                                const num = Number(val).toFixed(decimal);
+                                if (Number(num) > yestclose) return colors.upColor;
+                                if (Number(num) < yestclose) return colors.downColor;
+                            }
                             return '#7a8a94';
                         },
                     },
@@ -113,10 +121,12 @@ export function useOptions(state: State) {
                     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;
+                        color: (val) => {
+                            if (val) {
+                                const num = Number(val).toFixed(decimal);
+                                if (Number(num) > yestclose) return colors.upColor;
+                                if (Number(num) < yestclose) return colors.downColor;
+                            }
                             return '#7a8a94';
                         },
                     },
@@ -164,13 +174,13 @@ export function useOptions(state: State) {
                         },
                         data: [
                             {
-                                yAxis: dataset.source.close[dataset.source.close.length - 1] ?? '--', // 最新价
+                                yAxis: timeline.source.close[timeline.source.close.length - 1] ?? '--', // 最新价
                                 lineStyle: {
                                     color: colors.seriesMarkLineColor
                                 },
                             },
                             {
-                                yAxis: yestClose, // 昨日收盘价
+                                yAxis: yestclose, // 昨日收盘价
                             },
                         ],
                     },
@@ -188,18 +198,16 @@ export function useOptions(state: State) {
                 },
             ],
         }
-
-        onReady && onReady();
     }
 
     // 动态更新数据
     const updateOptions = throttle(() => {
-        const { seriesMarkLineColor } = getColors();
-        const { dataset, yestClose } = state;
+        const { colors } = options;
+        const { timeline, yestclose } = dataset;
 
-        timeOption.value = {
+        options.timeline = {
             dataset: {
-                source: dataset.source,
+                source: timeline.source,
             },
             series: [
                 {
@@ -207,13 +215,13 @@ export function useOptions(state: State) {
                     markLine: {
                         data: [
                             {
-                                yAxis: dataset.source.close[dataset.source.close.length - 1] ?? '--', // 最新价
+                                yAxis: timeline.source.close[timeline.source.close.length - 1] ?? '--', // 最新价
                                 lineStyle: {
-                                    color: seriesMarkLineColor
+                                    color: colors.seriesMarkLineColor
                                 },
                             },
                             {
-                                yAxis: yestClose, // 昨日收盘价
+                                yAxis: yestclose, // 昨日收盘价
                             },
                         ],
                     },
@@ -222,26 +230,14 @@ export function useOptions(state: State) {
         }
     }, 1000)
 
-    // 计算涨跌幅百分比,涨跌幅=(今日收盘价-昨日收盘价)/昨日收盘价*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 formatDecimal(result) + '%';
-        }
-        return '0%';
-    }
-
     // 监听主题变化
-    watch(theme, () => initOptions());
+    watch(theme, () => {
+        options.colors = getColors();
+        initOptions();
+    })
 
     return {
-        timeOption,
+        options,
         initOptions,
         updateOptions,
     }

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

@@ -1,59 +0,0 @@
-<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>

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

@@ -1,103 +0,0 @@
-import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue'
-import { debounce } from '@/utils/time'
-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 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);
-                    }
-                }
-            })
-
-            // 图表重置大小
-            const chartResize = debounce(() => {
-                chart.resize && chart.resize();
-            }, 50);
-
-            // 监听元素变化
-            new ResizeObserver(() => {
-                chartResize();
-            }).observe(el);
-
-            context?.emit('ready', chart);
-        }
-    })
-
-    onUnmounted(() => {
-        chart?.dispose();
-    })
-
-    return {
-        chartElement,
-        showLoading,
-        setOptions
-    }
-}

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

@@ -1,311 +0,0 @@
-<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="activeSeriesType === 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="activeSeriesType === 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="activeSeriesType === 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="activeSeriesType === 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" @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 { 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'
-import { Tabbar } from '../../tabbar/type'
-
-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), // 当前数据索引值
-      activeSeriesType = ref(SeriesType.MACD), // 当前选中的指标
-      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: Tabbar[] = [
-      { label: 'MACD', value: SeriesType.MACD },
-      { label: 'VOL', value: SeriesType.VOL },
-      { label: 'KDJ', value: SeriesType.KDJ },
-      { label: 'CCI', value: SeriesType.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 = (item: Tabbar<SeriesType>) => {
-      activeSeriesType.value = item.value;
-      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), // 实时行情最新时间
-        last = 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: last,
-            close: last,
-            lowest: last,
-            highest: last,
-            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 > last) {
-            item.lowest = last; // 更新最低价
-          }
-          if (item.highest < last) {
-            item.highest = last; // 更新最高价
-          }
-          item.close = last; // 更新收盘价
-        }
-
-        // 更新各种指标
-        calcIndicator(lastIndex === -1 ? 0 : lastIndex);
-        updateOptions();
-      }
-    }
-
-    // 监听行情推送
-    watch(() => quote.value.last, () => {
-      if (!loading.value) {
-        updateChartData();
-      }
-    })
-
-    // 监听周期选择变化
-    watch(() => props.cycleType, () => initData());
-
-    return {
-      SeriesType,
-      loading,
-      showEmpty,
-      dataIndex,
-      tabs,
-      activeSeriesType,
-      klineData,
-      klineOption,
-      macdOption,
-      volOption,
-      kdjOption,
-      cciOption,
-      klineDetail,
-      macdDetail,
-      volDetail,
-      kdjDetail,
-      cciDetail,
-      tabChange,
-      mainReady,
-      indicatorReady,
-    }
-  }
-})
-</script>
-
-<style lang="less">
-@import './index.less';
-</style>

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

@@ -1,131 +0,0 @@
-<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 { 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(updateChartData));
-        } 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);
-        updateOptions();
-      }
-    }
-
-    // 监听行情推送
-    watch(() => quote.value.last, () => {
-      if (!loading.value) {
-        updateChartData();
-      }
-    })
-
-    return {
-      loading,
-      showEmpty,
-      dataIndex,
-      timeOption,
-      timeDetail,
-      initData,
-    }
-  }
-})
-</script>
-
-<style lang="less">
-@import './index.less';
-</style>

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

@@ -1,40 +0,0 @@
-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,
-    seriesMarkLabelColor: string,
-    seriesMarkLineColor: string,
-    seriesAreaGradients: LinearGradientObject,
-}

+ 17 - 31
src/common/components/echarts/index.vue

@@ -1,20 +1,22 @@
 <template>
   <div class="mtp-chart">
-    <mtp-tabbar class="mtp-chart__tab" theme="menu" :data="chartType" @change="changeCycleType" />
-    <!-- K线  -->
-    <mtp-echarts-timeline class="mtp-chart__content" :goodscode="goodscode" @ready="onReady" v-if="activeCycleType === CycleType.time" />
-    <!-- 分时线  -->
-    <mtp-echarts-kline class="mtp-chart__content" :cycle-type="activeCycleType" :show-indicator="showIndicator" :goodscode="goodscode" v-else />
+    <mtp-tabbar class="mtp-chart__tab" theme="menu" :data="chartCycleTypeList" @change="changeCycleType" />
+    <!-- 分时线 -->
+    <mtp-echarts-timeline class="mtp-chart__content" :goodscode="goodscode"
+      v-if="activeCycleType === ChartCycleType.time" />
+    <!-- K线 -->
+    <mtp-echarts-kline class="mtp-chart__content" :cycle-type="activeCycleType" :show-indicator="showIndicator"
+      :goodscode="goodscode" v-else />
   </div>
 </template>
 
 <script lang="ts">
 import { defineComponent, ref } from 'vue'
-import { CycleType } from './echarts-kline/type';
-import MtpEchartsKline from './echarts-kline/index.vue';
-import MtpEchartsTimeline from './echarts-timeline/index.vue';
-import MtpTabbar from '../tabbar/index.vue';
-import { Tabbar } from '../tabbar/type';
+import { ChartCycleType, chartCycleTypeList } from '@/@next/constants/enum/chart'
+import MtpEchartsKline from '@/@next/components/modules/echarts-kline/index.vue'
+import MtpEchartsTimeline from '@/@next/components/modules/echarts-timeline/index.vue'
+import MtpTabbar from '../tabbar/index.vue'
+import { Tabbar } from '../tabbar/type'
 
 export default defineComponent({
   emits: ['ready'],
@@ -34,35 +36,19 @@ export default defineComponent({
       default: true,
     },
   },
-  setup(props, { emit }) {
-    const activeCycleType = ref(CycleType.time); // 当前选中的图表周期
-
-    // 周期类型
-    const chartType: Tabbar[] = [
-      { label: '分时', value: CycleType.time },
-      { label: '1分钟', value: CycleType.minutes },
-      { label: '5分钟', value: CycleType.minutes5 },
-      { label: '30分钟', value: CycleType.minutes30 },
-      { label: '60分钟', value: CycleType.minutes60 },
-      { label: '4小时', value: CycleType.Hours4 },
-      { label: '日K', value: CycleType.days },
-    ];
+  setup() {
+    const activeCycleType = ref(ChartCycleType.time); // 当前选中的图表周期
 
     // 切换图表周期类型
-    const changeCycleType = (item: Tabbar<CycleType>) => {
+    const changeCycleType = (item: Tabbar<ChartCycleType>) => {
       activeCycleType.value = item.value;
-    };
-
-    const onReady = (startTime: string, endTime: string) => {
-      emit('ready', startTime, endTime);
     }
 
     return {
-      CycleType,
-      chartType,
+      ChartCycleType,
+      chartCycleTypeList,
       activeCycleType,
       changeCycleType,
-      onReady,
     }
   }
 })

+ 1 - 1
src/views/hedgeditem/exposure/list/index.vue

@@ -101,7 +101,7 @@ export default defineComponent({
 })
 </script>
 
-<style lang="less">
+<style lang="less" scoped>
 .real-time-header {
     margin-left: auto;
     padding: 6px;

+ 143 - 0
src/views/hedgeditem/futures/in/components/add/form.ts

@@ -0,0 +1,143 @@
+import { ref, computed } from 'vue'
+import { v4 } from 'uuid'
+import Long from 'long'
+import { message } from 'ant-design-vue'
+import { geLoginID_number } from '@/services/bus/login'
+import { queryTableList, handleComposeTable } from '@/common/export/commonTable'
+import { InternalEnableTradeDetailReq, InternalEnableTradeDetailRsp, InternalUncorrelatedTradeDetailRsp } from '@/services/go/ermcp/hedgedItem/interface'
+import { queryInternalEnableTradeDetail } from '@/services/go/ermcp/hedgedItem'
+import { InnerTradeLinkRsp } from '@/services/proto/hedgedItem/interface'
+import { innerTradeLink } from '@/services/proto/hedgedItem'
+import { getAreaUserId } from '@/services/bus/user'
+
+export function useForm(selectedRow: InternalUncorrelatedTradeDetailRsp) {
+    const { tradeid, goodsid, tradelot } = selectedRow;
+    const { tableList, queryTable } = queryTableList<InternalEnableTradeDetailRsp>(true, 2); // 表格列表数据
+    const loading = ref<boolean>(false);
+    const selectedRowKeys = ref<number[]>([]); // 表格选中的 rowKey 数据 :rowKey="(record,index)=>index"
+
+    // 计算当前关联数量
+    const currentQty = computed(() => {
+        return tableList.value.reduce((pre, cur, index) => {
+            if (selectedRowKeys.value.includes(index)) {
+                pre += cur?.relatedlot ?? 0;
+            }
+            return pre;
+        }, 0);
+    })
+
+    // 自定义表格选择项
+    const rowSelection = computed(() => ({
+        columnTitle: '选择',
+        selectedRowKeys: selectedRowKeys.value,
+        onChange: (keys: number[]) => {
+            selectedRowKeys.value = keys;
+        },
+        onSelect: (record: InternalEnableTradeDetailRsp, selected: boolean) => {
+            if (!selected) {
+                // 未选中的取消关联量
+                record.relatedlot = undefined;
+            }
+        },
+    }))
+
+    // 计算本次关联数量
+    const calcRelatedqty = (record: InternalEnableTradeDetailRsp) => {
+        const { relatedlot, agreeunit, convertratio } = record;
+        if (relatedlot) {
+            return relatedlot * agreeunit * convertratio;
+        }
+        return 0;
+    }
+
+    // 表格通用逻辑
+    const composeTable = handleComposeTable<InternalEnableTradeDetailRsp>({
+        tableName: 'table_pcweb_hedgeditem_futures_in_relation',
+        queryFn: () => {
+            const param: InternalEnableTradeDetailReq = {
+                areauserid: getAreaUserId(),
+                goodsid: goodsid,
+            }
+            queryTable(queryInternalEnableTradeDetail, param);
+        },
+    })
+
+    // 表格列
+    const columns = computed(() => {
+        const result = composeTable.columns.value;
+        result.push(...[
+            {
+                title: '关联手数',
+                align: 'center',
+                key: 'relatedlot',
+                slots: {
+                    customRender: 'relatedlot'
+                }
+            },
+            {
+                title: '关联数量',
+                align: 'center',
+                key: 'relatedqty',
+                slots: {
+                    customRender: 'relatedqty'
+                }
+            }
+        ])
+        return result;
+    })
+
+    // 表单提交
+    const formSubmit = (callback?: () => void) => {
+        const keys = selectedRowKeys.value;
+        const seledtedRows = tableList.value.filter((e, index) => keys.includes(index));
+
+        if (seledtedRows.length) {
+            if (seledtedRows.every((e) => e.relatedlot && e.relatedlot > 0)) {
+                if (currentQty.value > tradelot) {
+                    message.error('关联数量不能大于成交手数');
+                } else {
+                    loading.value = true;
+                    const result: Promise<InnerTradeLinkRsp>[] = [];
+
+                    seledtedRows.forEach((e) => {
+                        result.push(innerTradeLink({
+                            TradeId: Long.fromString(tradeid), // 内部成交单号
+                            HedgeFlag: 13, // 投机套保标志
+                            SpotContractID: Long.fromString(e.hedgeditemid), // 现货合同ID
+                            RelatedLot: e.relatedlot!, // 关联手数
+                            RelatedMode: 2, // 关联模式
+                            CreatorSrc: 2, // 创建来源
+                            CreatorID: geLoginID_number()!, // 创建人
+                            ClientTicket: v4() // 客户端流水号
+                        }))
+                    })
+
+                    Promise.all(result).then(() => {
+                        message.success('关联成功');
+                        callback && callback();
+                    }).catch((err) => {
+                        message.error('关联失败:' + err);
+                    }).finally(() => {
+                        loading.value = false;
+                    })
+                }
+
+            } else {
+                message.error('请输入关联手数');
+            }
+        } else {
+            message.error('请选择关联订单');
+        }
+    }
+
+    return {
+        loading,
+        tableList,
+        selectedRowKeys,
+        rowSelection,
+        currentQty,
+        columns,
+        calcRelatedqty,
+        formSubmit,
+    }
+}

+ 135 - 0
src/views/hedgeditem/futures/in/components/add/index.vue

@@ -0,0 +1,135 @@
+<template>
+    <!-- 套期交易-期货成交关联-补单关联 -->
+    <a-modal class="commonModal custom-detail" title="期现单据关联" v-model:visible="visible" centered @cancel="cancel(false)"
+        :maskClosable="false" width="1000px">
+        <a-form class="inlineForm">
+            <fieldset class="formFieldSet">
+                <legend>成交单信息</legend>
+                <a-row :gutter="24">
+                    <a-col :span="12">
+                        <a-form-item label="期货合约">
+                            <span class="white">{{ selectedRow.accountname + '/' + selectedRow.accountid }}</span>
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="12">
+                        <a-form-item label="方向">
+                            <span class="white">{{ selectedRow.goodscode + '/' + selectedRow.goodsname }}</span>
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="12">
+                        <a-form-item label="期货子账户">
+                            <span class="white">{{ getChannelBuildName(selectedRow.channelbuildtype) }} {{
+                                    getBuyOrSellName(selectedRow.buyorsell)
+                            }}</span>
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="12">
+                        <a-form-item label="建平">
+                            <span class="white">{{ formatValue(selectedRow.tradeprice) }}</span>
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="12">
+                        <a-form-item label="成交价">
+                            <span class="white">{{ formatValue(selectedRow.tradelot) }}</span>
+                        </a-form-item>
+                    </a-col>
+                    <a-col :span="12">
+                        <a-form-item label="成交数量">
+                            <span class="white">{{ formatValue(selectedRow.tradetime) }}</span>
+                        </a-form-item>
+                    </a-col>
+                </a-row>
+            </fieldset>
+            <fieldset class="formFieldSet">
+                <legend>关联信息</legend>
+                <div class="tableDatas">
+                    <a-table class="dialogTable" :columns="columns" :data-source="tableList" :pagination="false"
+                        :loading="loading" :rowKey="(record, index) => index" :row-selection="rowSelection">
+                        <template #wrstandardname="{ record }">
+                            <span>{{ record.deliverygoodsname }}/{{ record.wrstandardname }}</span>
+                        </template>
+                        <template #relatedlot="{ record, index }">
+                            <a-input-number class="dialogInput" size="small"
+                                :disabled="!selectedRowKeys.includes(index)" :precision="0"
+                                v-model:value="record.relatedlot" style="width:80px" />
+                        </template>
+                        <template #relatedqty="{ record }">
+                            <span>{{ calcRelatedqty(record) }}</span>
+                        </template>
+                    </a-table>
+                </div>
+            </fieldset>
+        </a-form>
+        <template #footer>
+            <a-button key="submit" class="cancelBtn" :loading="loading" @click="cancel(false)">取消</a-button>
+            <a-button key="submit" type="primary" :loading="loading" @click="formSubmit(() => cancel(true))">确定
+            </a-button>
+        </template>
+    </a-modal>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue'
+import { _closeModal } from '@/common/setup/modal/modal'
+import { formatValue } from '@/common/methods'
+import { getChannelBuildName, getBuyOrSellName } from '@/common/constants/enumsName'
+import { InternalUncorrelatedTradeDetailRsp } from '@/services/go/ermcp/hedgedItem/interface'
+import { useForm } from './form'
+
+export default defineComponent({
+    emits: ['cancel'],
+    props: {
+        selectedRow: {
+            type: Object as PropType<InternalUncorrelatedTradeDetailRsp>,
+            required: true
+        },
+    },
+    setup(props, context) {
+        const { visible, cancel } = _closeModal(context);
+        const { loading, tableList, columns, currentQty, selectedRowKeys, rowSelection, calcRelatedqty, formSubmit } = useForm(props.selectedRow);
+
+        return {
+            visible,
+            loading,
+            tableList,
+            columns,
+            cancel,
+            formatValue,
+            currentQty,
+            selectedRowKeys,
+            rowSelection,
+            calcRelatedqty,
+            formSubmit,
+            getBuyOrSellName,
+            getChannelBuildName,
+        }
+    }
+})
+</script>
+
+<style lang="less">
+.custom-detail {
+    .ant-modal-body {
+        padding-top: 0;
+        padding-left: 0;
+        padding-right: 0;
+    }
+
+    .tableDatas {
+        margin-top: 26px;
+        padding: 0;
+        overflow: hidden;
+
+        .dialogTable {
+            width: 100%;
+            max-height: 230px;
+            overflow-x: auto !important;
+        }
+    }
+
+    .ant-form.inlineForm {
+        margin-top: 20px;
+        padding: 0 24px;
+    }
+}
+</style>

+ 12 - 4
src/views/hedgeditem/futures/in/index.vue

@@ -2,14 +2,17 @@
     <!-- 套期交易-期货成交关联-内部未关联成交单查询 -->
     <mtp-table-scroll>
         <template #header>
-            <Filter @search="updateColumn"></Filter>
+            <Filter @search="updateColumn">
+                <mtp-table-button class="btn-list-sticky" :buttons="headerButtons" @click="openComponent" />
+            </Filter>
         </template>
         <template #default="{ scroll }">
             <a-table :columns="columns" class="srcollYTable" :pagination="false" :loading="loading"
                 :expandedRowKeys="expandedRowKeys" :customRow="Rowclick" :rowKey="(record, index) => index"
                 :data-source="tableList" :scroll="scroll">
                 <template #expandedRowRender="{ record }">
-                    <mtp-table-button class="btn-list-sticky" :buttons="auth" :record="record" @click="openComponent" />
+                    <mtp-table-button class="btn-list-sticky" :buttons="tableButtons" :record="record"
+                        @click="openComponent" />
                 </template>
                 <!-- 套期主体 -->
                 <template #accountname="{ record }">
@@ -44,12 +47,16 @@ export default defineComponent({
         MtpTableButton,
         MtpTableScroll,
         Filter,
+        add: defineAsyncComponent(() => import('./components/add/index.vue')), // 补单关联
         bind: defineAsyncComponent(() => import('./components/bind/index.vue')), // 关联
     },
     setup() {
-        const { auth } = useMenu();
+        const { authFilter } = useMenu();
         const { loading, tableList, queryTable } = queryTableList<InternalUncorrelatedTradeDetailRsp>(true, 2); // 表格列表数据
 
+        const headerButtons = authFilter(['add']);
+        const tableButtons = authFilter(['add'], true);
+
         const queryFn = () => {
             const param: InternalUncorrelatedTradeDetailReq = {
                 userid: getUserId()
@@ -65,7 +72,8 @@ export default defineComponent({
 
         return {
             ...composeTable,
-            auth,
+            headerButtons,
+            tableButtons,
             loading,
             tableList,
             getBuyOrSellName,

+ 3 - 2
src/views/hedgeditem/spot/contract/index.vue

@@ -5,8 +5,9 @@
             <Filter @search="updateColumn"></Filter>
         </template>
         <template #default="{ scroll }">
-            <a-table :columns="columns" class="srcollYTable" :pagination="false" :expandedRowKeys="expandedRowKeys"
-                :customRow="Rowclick" :rowKey="(record, index) => index" :data-source="tableList" :scroll="scroll">
+            <a-table :columns="columns" class="srcollYTable" :pagination="false" :loading="loading"
+                :expandedRowKeys="expandedRowKeys" :customRow="Rowclick" :rowKey="(record, index) => index"
+                :data-source="tableList" :scroll="scroll">
                 <template #expandedRowRender="{ record }">
                     <mtp-table-button class="btn-list-sticky" :buttons="auth" :record="record" @click="openComponent" />
                 </template>

+ 1 - 1
src/views/hedgeditem/spot/plan/components/bind/index.vue

@@ -4,7 +4,7 @@
         :maskClosable="false" width="890px">
         <a-form class="inlineForm">
             <fieldset class="formFieldSet">
-                <legend>合同信息</legend>
+                <legend>计划信息</legend>
                 <a-row :gutter="24">
                     <a-col :span="12">
                         <a-form-item label="计划名称">

+ 3 - 2
src/views/hedgeditem/spot/plan/index.vue

@@ -5,8 +5,9 @@
             <Filter @search="updateColumn"></Filter>
         </template>
         <template #default="{ scroll }">
-            <a-table :columns="columns" class="srcollYTable" :pagination="false" :expandedRowKeys="expandedRowKeys"
-                :customRow="Rowclick" :rowKey="(record, index) => index" :data-source="tableList" :scroll="scroll">
+            <a-table :columns="columns" class="srcollYTable" :pagination="false" :loading="loading"
+                :expandedRowKeys="expandedRowKeys" :customRow="Rowclick" :rowKey="(record, index) => index"
+                :data-source="tableList" :scroll="scroll">
                 <template #expandedRowRender="{ record }">
                     <mtp-table-button class="btn-list-sticky" :buttons="auth" :record="record" @click="openComponent" />
                 </template>

+ 73 - 7
src/views/hedgeditem/trade/running/index.vue

@@ -4,6 +4,14 @@
         <template #header>
             <Filter @search="updateColumn">
                 <mtp-table-button class="btn-list-sticky" :buttons="headerButtons" @click="openComponent" />
+                <div class="real-time-header">
+                    <span class="dialogSpan">每隔</span>
+                    <a-input-number class="commonInput real-time-select" :min="10" :max="60" :disabled="isStart"
+                        v-model:value="second"></a-input-number>
+                    <span class="dialogSpan">秒刷新一次,倒计时 </span> <span class="red">{{ counter }}</span>
+                    <a-button class="operBtn ant-btn" @click="setTimerAction">{{ isStart ? '停止监控' : '开始监控' }}
+                    </a-button>
+                </div>
             </Filter>
         </template>
         <template #default="{ scroll }">
@@ -11,7 +19,7 @@
                 :rowClassName="rowClassName" :expandedRowKeys="expandedRowKeys" :customRow="Rowclick"
                 :rowKey="(record, index) => index" :data-source="dataList" :scroll="scroll">
                 <template #expandedRowRender="{ record }">
-                    <mtp-table-button class="btn-list-sticky" :buttons="tableButtons" :record="record"
+                    <mtp-table-button class="btn-list-sticky" :buttons="tableButtons(record)" :record="record"
                         @click="openComponent" />
                 </template>
                 <!-- 套期类型 -->
@@ -42,7 +50,7 @@
 </template>
 
 <script lang="ts">
-import { computed } from 'vue'
+import { computed, ref } from 'vue'
 import { queryTableList, MtpTableButton, defineAsyncComponent, defineComponent, handleComposeTable } from '@/common/export/commonTable'
 import { getHedgedTypeName } from '@/@next/constants/enum/hedgedType'
 import { Ermcp8RunningHedgeditemReq, Ermcp2HedgedItemRsp, HedgedItemMiddleGoodsList } from '@/services/go/ermcp/hedgedItem/interface'
@@ -67,18 +75,56 @@ export default defineComponent({
     setup() {
         const { authFilter } = useMenu();
         const { loading, tableList, queryTable } = queryTableList<Ermcp2HedgedItemRsp>(true, 2); // 表格列表数据
+        const timer = ref(0);
+        const second = ref(10); // 倒计时秒数
+        const counter = ref(0); // 倒计时计数器
+        const isStart = ref(false);
 
         const headerButtons = authFilter(['add']);
-        const tableButtons = authFilter(['add'], true);
+        const tableButtons = (item: Ermcp2HedgedItemRsp) => {
+            const buttons = authFilter(['add'], true);
+            if (item.wrstandardid) {
+                return buttons;
+            }
+            return buttons.filter((e) => e.code !== 'trade');
+        }
+
+        const setTimerAction = () => {
+            clearInterval(timer.value)
+            isStart.value = !isStart.value;
+            counter.value = 0;
+
+            if (isStart.value) {
+                counter.value = second.value;
+                countdown();
+            }
+        }
+
+        const countdown = () => {
+            timer.value = window.setInterval(() => {
+                if (counter.value) {
+                    counter.value--;
+                } else {
+                    counter.value = second.value;
+                    queryFn();
+                }
+            }, 1000);
+        }
 
         const queryFn = () => {
             const param: Ermcp8RunningHedgeditemReq = {
                 userid: getUserId(),
             }
             queryTable(queryErmcp8RunningHedgeditem, param).then((res) => {
-                if (res.length) {
-                    composeTable.selectedRow.value = res[0];
-                }
+                const selectedRow = composeTable.selectedRow.value;
+                composeTable.selectedRow.value = undefined;
+
+                window.setTimeout(() => {
+                    composeTable.selectedRow.value = selectedRow;
+                    if (!selectedRow && res.length) {
+                        composeTable.selectedRow.value = dataList.value[0];
+                    }
+                }, 0)
             })
         }
 
@@ -156,9 +202,29 @@ export default defineComponent({
             dataList,
             headerButtons,
             tableButtons,
+            second,
+            counter,
+            isStart,
+            setTimerAction,
             getDecimalsNum,
             getHedgedTypeName,
         }
     }
 })
-</script>
+</script>
+
+<style lang="less" scoped>
+.real-time-header {
+    margin-left: auto;
+    padding: 0;
+}
+
+.real-time-select {
+    margin-left: 5px;
+    margin-right: 5px;
+
+    .ant-input-number-input {
+        text-align: center;
+    }
+}
+</style>

+ 66 - 34
src/views/information/spot-contract/components/modify/index.vue

@@ -1,6 +1,7 @@
 <template>
   <!-- 重新提交现货合同-->
-  <a-modal class="commonModal modify-custom" title="重新提交现货合同" v-if="visible" v-model:visible="visible" @cancel="cancel" centered width="890px">
+  <a-modal class="commonModal modify-custom" title="重新提交现货合同" v-if="visible" v-model:visible="visible" @cancel="cancel"
+    centered width="890px">
     <template #footer>
       <a-button key="submit" type="primary" :loading="loading" :disabled="loading" @click="submit">完成</a-button>
     </template>
@@ -10,12 +11,14 @@
         <a-row :gutter="24">
           <a-col :span="12">
             <a-form-item label="合同编号" name="ContractNo">
-              <a-input class="dialogInput" style="width: 200px" v-model:value="formState.ContractNo" placeholder="请输入合同编号" />
+              <a-input class="dialogInput" style="width: 200px" v-model:value="formState.ContractNo"
+                placeholder="请输入合同编号" />
             </a-form-item>
           </a-col>
           <a-col :span="12">
             <a-form-item label="合同类型" name="ContractType">
-              <a-select class="inlineFormSelect" style="width: 200px" @change="contractChange" v-model:value="formState.ContractType" placeholder="请选择合同类型">
+              <a-select class="inlineFormSelect" style="width: 200px" @change="contractChange"
+                v-model:value="formState.ContractType" placeholder="请选择合同类型">
                 <a-select-option v-for="item in contractType" :key="item.key" :value="item.key">
                   {{ item.value }}
                 </a-select-option>
@@ -24,7 +27,8 @@
           </a-col>
           <a-col :span="12">
             <a-form-item label="业务类型" name="BizType">
-              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.BizType" placeholder="请选择业务类型">
+              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.BizType"
+                placeholder="请选择业务类型">
                 <a-select-option v-for="item in businessType" :key="item.key" :value="item.key">
                   {{ item.value }}
                 </a-select-option>
@@ -34,7 +38,8 @@
           <template v-if="isSell">
             <a-col :span="12">
               <a-form-item label="采购方" name="BuyUserID">
-                <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.BuyUserID" placeholder="请选择客户">
+                <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.BuyUserID"
+                  placeholder="请选择客户">
                   <!-- 客户资料列表 正常 -->
                   <a-select-option :value="item.userid" v-for="item in customList" :key="item.userid">
                     {{ item.customername }}
@@ -56,7 +61,8 @@
             </a-col>
             <a-col :span="12">
               <a-form-item label="销售方" name="SellUserID">
-                <a-select class="inlineFormSelect" v-model:value="formState.SellUserID" style="width: 200px" placeholder="请选择客户">
+                <a-select class="inlineFormSelect" v-model:value="formState.SellUserID" style="width: 200px"
+                  placeholder="请选择客户">
                   <!-- 客户资料列表 正常 -->
                   <a-select-option :value="item.userid" v-for="item in customList" :key="item.userid">
                     {{ item.customername }}
@@ -72,7 +78,8 @@
           </a-col>
           <a-col :span="12">
             <a-form-item label="交易主体" name="SubjectID">
-              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.SubjectID" placeholder="请选择主体">
+              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.SubjectID"
+                placeholder="请选择主体">
                 <a-select-option :value="item.subjectid" v-for="item in subjectList" :key="item.subjectid">
                   {{ item.subjectname }}
                 </a-select-option>
@@ -86,8 +93,10 @@
         <a-row :gutter="24">
           <a-col :span="12">
             <a-form-item label="现货品种" name="DeliveryGoodsID">
-              <a-select class="inlineFormSelect" style="width: 200px" @change="deliveryGoodsChange" v-model:value="formState.DeliveryGoodsID" placeholder="请选择现货品种">
-                <a-select-option v-for="item in deliveryGoodsList" :key="item.deliverygoodsid" :value="item.deliverygoodsid">
+              <a-select class="inlineFormSelect" style="width: 200px" @change="deliveryGoodsChange"
+                v-model:value="formState.DeliveryGoodsID" placeholder="请选择现货品种">
+                <a-select-option v-for="item in deliveryGoodsList" :key="item.deliverygoodsid"
+                  :value="item.deliverygoodsid">
                   {{ item.deliverygoodsname }}
                 </a-select-option>
               </a-select>
@@ -95,7 +104,8 @@
           </a-col>
           <a-col :span="12">
             <a-form-item label="商品" name="WrStandardID">
-              <a-select class="inlineFormSelect" style="width: 200px" @change="wrStandardChange" v-model:value="formState.WrStandardID" placeholder="请选择商品">
+              <a-select class="inlineFormSelect" style="width: 200px" @change="wrStandardChange"
+                v-model:value="formState.WrStandardID" placeholder="请选择商品">
                 <a-select-option v-for="item in wrstandardList" :key="item.wrstandardid" :value="item.wrstandardid">
                   {{ item.wrstandardname }}
                 </a-select-option>
@@ -104,7 +114,8 @@
           </a-col>
           <a-col :span="12">
             <a-form-item label="品牌" name="SpotGoodsBrandID">
-              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.SpotGoodsBrandID" placeholder="请选择品牌">
+              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.SpotGoodsBrandID"
+                placeholder="请选择品牌">
                 <a-select-option v-for="item in barandList" :key="item.brandid" :value="item.brandid">
                   {{ item.brandname }}
                 </a-select-option>
@@ -113,7 +124,8 @@
           </a-col>
           <a-col :span="12">
             <a-form-item label="商品规格" name="SpotGoodsDesc">
-              <a-input class="dialogInput" style="width: 200px" v-model:value="formState.SpotGoodsDesc" placeholder="请输入商品规格" />
+              <a-input class="dialogInput" style="width: 200px" v-model:value="formState.SpotGoodsDesc"
+                placeholder="请输入商品规格" />
             </a-form-item>
           </a-col>
         </a-row>
@@ -123,7 +135,8 @@
         <a-row :gutter="24">
           <a-col :span="12">
             <a-form-item label="结算币种" name="CurrencyID">
-              <a-select class="inlineFormSelect" v-model:value="formState.CurrencyID" style="width: 200px" placeholder="请选择结算币种">
+              <a-select class="inlineFormSelect" v-model:value="formState.CurrencyID" style="width: 200px"
+                placeholder="请选择结算币种">
                 <a-select-option v-for="item in payCurrency" :key="item.enumitemname" :value="item.enumitemname">
                   {{ item.enumdicname }}
                 </a-select-option>
@@ -132,17 +145,20 @@
           </a-col>
           <a-col :span="12">
             <a-form-item label="数量" name="Qty">
-              <a-input class="dialogInput suffixGrey" v-model:value="formState.Qty" style="width: 200px" @change="getAmout" placeholder="请输入数量" :suffix="numberUnit" />
+              <a-input class="dialogInput suffixGrey" v-model:value="formState.Qty" style="width: 200px"
+                @change="getAmout" placeholder="请输入数量" :suffix="numberUnit" />
             </a-form-item>
           </a-col>
           <a-col :span="12">
             <a-form-item label="交收期">
-              <a-range-picker v-model:value="deliveryDate" class="commonPicker" style="width: 200px" :disabled-date="disabledDate" :show-time="{ hideDisabledOptions: true }" format="YYYY-MM-DD" />
+              <a-range-picker v-model:value="deliveryDate" class="commonPicker" style="width: 200px"
+                :disabled-date="disabledDate" :show-time="{ hideDisabledOptions: true }" format="YYYY-MM-DD" />
             </a-form-item>
           </a-col>
           <a-col :span="12">
             <a-form-item label="定价类型" name="PriceType">
-              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.PriceType" placeholder="请选择定价类型">
+              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.PriceType"
+                placeholder="请选择定价类型">
                 <a-select-option v-for="item in priceType" :key="item.key" :value="item.key">
                   {{ item.value }}
                 </a-select-option>
@@ -153,7 +169,8 @@
           <template v-if="formState.PriceType === 2 || formState.PriceType === 3">
             <a-col :span="12">
               <a-form-item label="点价类型">
-                <a-select class="inlineFormSelect" v-model:value="pricingType" style="width: 200px" placeholder="请选择点价类型" @change="pricingTypeChange">
+                <a-select class="inlineFormSelect" v-model:value="pricingType" style="width: 200px"
+                  placeholder="请选择点价类型" @change="pricingTypeChange">
                   <a-select-option :value="0">现货</a-select-option>
                   <a-select-option :value="1">期货</a-select-option>
                 </a-select>
@@ -161,7 +178,8 @@
             </a-col>
             <a-col :span="12" v-if="pricingType === 1">
               <a-form-item label="点价合约" name="GoodsID">
-                <a-select class="inlineFormSelect" v-model:value="formState.GoodsID" style="width: 200px" placeholder="请选择点价合约">
+                <a-select class="inlineFormSelect" v-model:value="formState.GoodsID" style="width: 200px"
+                  placeholder="请选择点价合约">
                   <a-select-option v-for="item in goodsList" :key="item.goodsid" :value="item.goodsid">
                     {{ item.goodsname }}
                   </a-select-option>
@@ -170,22 +188,20 @@
             </a-col>
             <a-col :span="12" v-if="formState.PriceType === 3">
               <a-form-item label="暂定价" name="Price">
-                <a-input class="dialogInput suffixGrey" style="width: 200px" v-model:value="formState.Price" :suffix="payCurrencyUnit" placeholder="请输入暂定价" />
-              </a-form-item>
-            </a-col>
-            <a-col :span="12">
-              <a-form-item label="升贴水" name="PriceMove">
-                <a-input-number class="dialogInput suffixGrey" v-model:value="formState.PriceMove" placeholder="请输入升贴水" :suffix="`${payCurrencyUnit}/${numberUnit}`" style="width: 200px" />
+                <a-input class="dialogInput suffixGrey" style="width: 200px" v-model:value="formState.Price"
+                  :suffix="payCurrencyUnit" placeholder="请输入暂定价" />
               </a-form-item>
             </a-col>
             <a-col :span="12">
               <a-form-item label="点价期">
-                <a-range-picker v-model:value="priceDate" class="commonPicker" :disabled-date="disabledDate" style="width: 200px" :show-time="{ hideDisabledOptions: true }" format="YYYY-MM-DD" />
+                <a-range-picker v-model:value="priceDate" class="commonPicker" :disabled-date="disabledDate"
+                  style="width: 200px" :show-time="{ hideDisabledOptions: true }" format="YYYY-MM-DD" />
               </a-form-item>
             </a-col>
             <a-col :span="24">
               <a-form-item label="点价说明" name="PointDesc">
-                <a-input class="dialogInput" style="width: 608px" v-model:value="formState.PointDesc" placeholder="请输入点价说明" />
+                <a-input class="dialogInput" style="width: 608px" v-model:value="formState.PointDesc"
+                  placeholder="请输入点价说明" />
               </a-form-item>
             </a-col>
           </template>
@@ -193,12 +209,20 @@
           <template v-if="formState.PriceType === 1">
             <a-col :span="12">
               <a-form-item label="价格" name="Price">
-                <a-input class="dialogInput suffixGrey" v-model:value="formState.Price" @change="getAmout" placeholder="请输入价格" :suffix="`${payCurrencyUnit}/${numberUnit}`" style="width: 200px" />
+                <a-input class="dialogInput suffixGrey" v-model:value="formState.Price" @change="getAmout"
+                  placeholder="请输入价格" :suffix="`${payCurrencyUnit}/${numberUnit}`" style="width: 200px" />
+              </a-form-item>
+            </a-col>
+            <a-col :span="12">
+              <a-form-item label="升贴水" name="PriceMove">
+                <a-input-number class="dialogInput suffixGrey" v-model:value="formState.PriceMove" placeholder="请输入升贴水"
+                  :suffix="`${payCurrencyUnit}/${numberUnit}`" style="width: 200px" @change="getAmout" />
               </a-form-item>
             </a-col>
             <a-col :span="12">
               <a-form-item label="金额" class="relative" name="Amount">
-                <a-input class="dialogInput suffixGrey" v-model:value="formState.Amount" readonly placeholder="输入数量和价格后自动算" :suffix="payCurrencyUnit" style="width: 200px" />
+                <a-input class="dialogInput suffixGrey" v-model:value="formState.Amount" readonly
+                  placeholder="输入数量和价格后自动算" :suffix="payCurrencyUnit" style="width: 200px" />
               </a-form-item>
             </a-col>
           </template>
@@ -209,13 +233,17 @@
         <a-row :gutter="24">
           <a-col :span="12">
             <a-form-item label="保证金" name="ContractMargin">
-              <a-input class="dialogInput suffixGrey" placeholder="请输入保证金" v-model:value="formState.ContractMargin" style="width: 200px" :suffix="payCurrencyUnit" />
+              <a-input class="dialogInput suffixGrey" placeholder="请输入保证金" v-model:value="formState.ContractMargin"
+                style="width: 200px" :suffix="payCurrencyUnit" />
             </a-form-item>
           </a-col>
           <a-col :span="12">
             <a-form-item label="业务员" name="SaleUserID">
-              <a-select class="inlineFormSelect" v-model:value="formState.SaleUserID" style="width: 200px" placeholder="请选择业务员">
-                <a-select-option v-for="item in businesserList" :key="item.userid" :value="item.userid"> {{ item.accountname }}-{{ item.logincode }} </a-select-option>
+              <a-select class="inlineFormSelect" v-model:value="formState.SaleUserID" style="width: 200px"
+                placeholder="请选择业务员">
+                <a-select-option v-for="item in businesserList" :key="item.userid" :value="item.userid"> {{
+                    item.accountname
+                }}-{{ item.logincode }} </a-select-option>
               </a-select>
             </a-form-item>
           </a-col>
@@ -223,14 +251,18 @@
         <a-row :gutter="24">
           <a-col :span="12">
             <a-form-item label="跟单员" name="MerUserID">
-              <a-select class="inlineFormSelect" v-model:value="formState.MerUserID" style="width: 200px" placeholder="请选择跟单员">
-                <a-select-option v-for="item in merchandiserList" :key="item.userid" :value="item.userid"> {{ item.accountname }}-{{ item.logincode }} </a-select-option>
+              <a-select class="inlineFormSelect" v-model:value="formState.MerUserID" style="width: 200px"
+                placeholder="请选择跟单员">
+                <a-select-option v-for="item in merchandiserList" :key="item.userid" :value="item.userid"> {{
+                    item.accountname
+                }}-{{ item.logincode }} </a-select-option>
               </a-select>
             </a-form-item>
           </a-col>
           <a-col :span="12">
             <a-form-item label="交易用户" name="TradeUserID">
-              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.TradeUserID" placeholder="请选择期货账户">
+              <a-select class="inlineFormSelect" style="width: 200px" v-model:value="formState.TradeUserID"
+                placeholder="请选择期货账户">
                 <a-select-option v-for="item in traderList" :key="item.roleid" :value="item.roleid">
                   {{ item.rolename }}
                 </a-select-option>