li.shaoyi 3 years ago
parent
commit
325f194d56
34 changed files with 2786 additions and 914 deletions
  1. 37 35
      src/business/account/index.ts
  2. 1 3
      src/components/base/echarts/index.ts
  3. 4 4
      src/components/base/echarts/index.vue
  4. 0 286
      src/components/modules/echarts-kline/index.vue
  5. 0 71
      src/components/modules/echarts-kline/interface.ts
  6. 0 83
      src/components/modules/echarts-timeline/dataset.ts
  7. 0 116
      src/components/modules/echarts-timeline/index.vue
  8. 0 28
      src/components/modules/echarts-timeline/interface.ts
  9. 0 83
      src/components/modules/echarts-timeline/options.ts
  10. 80 82
      src/hooks/echarts/candlestick/dataset.ts
  11. 211 0
      src/hooks/echarts/candlestick/index.ts
  12. 96 0
      src/hooks/echarts/candlestick/interface.ts
  13. 79 78
      src/hooks/echarts/candlestick/options.ts
  14. 146 0
      src/hooks/echarts/timeline/dataset.ts
  15. 106 0
      src/hooks/echarts/timeline/index.ts
  16. 49 0
      src/hooks/echarts/timeline/interface.ts
  17. 246 0
      src/hooks/echarts/timeline/options.ts
  18. 3 3
      src/hooks/theme/index.ts
  19. 1472 1
      src/mock/quote.ts
  20. 2 2
      src/packages/mobile/components/layouts/statusbar/index.vue
  21. 2 7
      src/packages/mobile/views/account/login/index.vue
  22. 3 3
      src/packages/mobile/views/boot/index.vue
  23. 0 0
      src/packages/pc/components/modules/echarts-kline/index.less
  24. 120 0
      src/packages/pc/components/modules/echarts-kline/index.vue
  25. 0 0
      src/packages/pc/components/modules/echarts-timeline/index.less
  26. 28 0
      src/packages/pc/components/modules/echarts-timeline/index.vue
  27. 8 9
      src/packages/pc/views/account/login/index.vue
  28. 2 2
      src/packages/pc/views/market/goods/index.vue
  29. 9 2
      src/services/api/quote/index.ts
  30. 1 1
      src/services/socket/index.ts
  31. 4 5
      src/services/socket/trade/protobuf/index.ts
  32. 33 2
      src/types/ermcp/quote.d.ts
  33. 34 0
      src/utils/time/index.ts
  34. 10 8
      src/utils/vant/index.ts

+ 37 - 35
src/business/account/index.ts

@@ -23,44 +23,46 @@ export function useAccount() {
     })
 
     // 用户登录
-    const userLogin = (callback?: (msg?: string) => void) => {
+    const userLogin = () => {
         loading.value = true;
-        return queryLoginId({
-            data: {
-                username: account.LoginID
-            },
-            success: (res) => {
-                login({
-                    data: {
-                        ...account,
-                        LoginID: res.data,
-                        LoginPWD: cryptojs.SHA256(res.data + account.LoginPWD).toString(),
-                    },
-                    success: (res) => {
-                        sessionCache.setValue('loginInfo', res);
+        return new Promise((resolve, reject) => {
+            queryLoginId({
+                data: {
+                    username: account.LoginID
+                },
+                success: (res) => {
+                    login({
+                        data: {
+                            ...account,
+                            LoginID: res.data,
+                            LoginPWD: cryptojs.SHA256(res.data + account.LoginPWD).toString(),
+                        },
+                        success: (res) => {
+                            sessionCache.setValue('loginInfo', res);
 
-                        initBaseData(() => {
-                            loading.value = false;
-                            callback && callback();
+                            initBaseData(() => {
+                                loading.value = false;
+                                resolve(res);
 
-                            const redirect = route.query.redirect;
-                            if (redirect) {
-                                router.replace(redirect.toString());
-                            } else {
-                                router.replace('/');
-                            }
-                        })
-                    },
-                    fail: (err) => {
-                        loading.value = false;
-                        callback && callback(err.msg);
-                    }
-                })
-            },
-            fail: (err) => {
-                loading.value = false;
-                callback && callback(err.msg);
-            }
+                                const redirect = route.query.redirect;
+                                if (redirect) {
+                                    router.replace(redirect.toString());
+                                } else {
+                                    router.replace('/');
+                                }
+                            })
+                        },
+                        fail: (err) => {
+                            loading.value = false;
+                            reject(err.msg);
+                        }
+                    })
+                },
+                fail: (err) => {
+                    loading.value = false;
+                    reject(err.msg);
+                }
+            })
         })
     }
 

+ 1 - 3
src/components/base/echarts/index.ts

@@ -71,9 +71,7 @@ export function useEcharts() {
                 if (chart.containPixel('grid', pointInPixel)) {
                     const pointInGrid = chart.convertFromPixel({ seriesIndex: 0 }, pointInPixel)
                     const dataIndex = pointInGrid[0];
-                    if (dataIndex > -1) {
-                        instance?.emit('update:index', dataIndex);
-                    }
+                    instance?.emit('update:index', dataIndex);
                 }
             })
 

+ 4 - 4
src/components/base/echarts/index.vue

@@ -1,11 +1,11 @@
 <template>
-  <div ref="chartElement" :class="['app-echarts', empty && 'is-empty']" :style="{ width: width, height: height }"></div>
+  <div ref="chartElement" class="app-echarts" :style="{ width: width, height: height }"></div>
 </template>
 
 <script lang="ts" setup>
-import { PropType, watch } from 'vue';
-import { EChartsOption } from 'echarts';
-import { useEcharts } from './index';
+import { PropType, watch } from 'vue'
+import { EChartsOption } from 'echarts'
+import { useEcharts } from './index'
 
 const props = defineProps({
   // 图表宽度

+ 0 - 286
src/components/modules/echarts-kline/index.vue

@@ -1,286 +0,0 @@
-<template>
-  <div class="app-echats-kline">
-    <div class="app-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>
-      <app-echarts :option="klineOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading"
-        @ready="mainReady" />
-    </div>
-    <template v-if="showIndicator">
-      <div class="app-echats-kline__container indicator">
-        <!-- MACD -->
-        <section class="section" v-if="activeSeriesType === EChartsSeriesType.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>
-          <app-echarts :option="macdOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading"
-            @ready="indicatorReady" />
-        </section>
-        <!-- VOL -->
-        <section class="section" v-if="activeSeriesType === EChartsSeriesType.VOL">
-          <ul class="legend">
-            <li class="legend-item">VOL: {{ volDetail ? volDetail.vol : '--' }}</li>
-          </ul>
-          <app-echarts :option="volOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading"
-            @ready="indicatorReady" />
-        </section>
-        <!-- KDJ -->
-        <section class="section" v-if="activeSeriesType === EChartsSeriesType.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>
-          <app-echarts :option="kdjOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading"
-            @ready="indicatorReady" />
-        </section>
-        <!-- CCI -->
-        <section class="section" v-if="activeSeriesType === EChartsSeriesType.CCI">
-          <ul class="legend">
-            <li class="legend-item">CCI: {{ cciDetail ? cciDetail.cci : '--' }}</li>
-          </ul>
-          <app-echarts :option="cciOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading"
-            @ready="indicatorReady" />
-        </section>
-      </div>
-      <app-tab theme="menu" :data-source="tabs" @change="tabChange" />
-    </template>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ref, reactive, PropType, watch, computed } from 'vue'
-import { queryHistoryDatas } from '@/services/api/quote'
-import { EChartsCycleType, EChartsSeriesType } from '@/constants/enum'
-import { useDataset } from './dataset'
-import { useOptions } from './options'
-import moment from 'moment';
-import * as echarts from 'echarts'
-import AppEcharts from '@/components/base/echarts/index.vue'
-import AppTab from '@/components/base/tab/index.vue'
-
-const props = defineProps({
-  goodsCode: String,
-  // 周期类型
-  cycleType: {
-    type: Number as PropType<EChartsCycleType>,
-    default: EChartsCycleType.minutes,
-  },
-  // 是否显示指标
-  showIndicator: {
-    type: Boolean,
-    default: true,
-  },
-})
-
-const loading = ref(false),
-  showEmpty = ref(false),
-  dataIndex = ref(0), // 当前数据索引值
-  activeSeriesType = ref(EChartsSeriesType.MACD), // 当前选中的指标
-  chartGroup = new Map<string, echarts.ECharts>(); // 图表联动实例组
-
-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 = [
-  { label: 'MACD', value: EChartsSeriesType.MACD },
-  { label: 'VOL', value: EChartsSeriesType.VOL },
-  { label: 'KDJ', value: EChartsSeriesType.KDJ },
-  { label: 'CCI', value: EChartsSeriesType.CCI },
-]
-
-// 模拟最新价推送
-const quote = reactive({
-  last: 0,
-  lasttime: new Date(),
-});
-
-// setInterval(() => {
-//   quote.last++;
-//   quote.lasttime = new Date();
-// }, 1000);
-
-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;
-  klineData.source = [];
-  macdData.source = [];
-  volData.source = [];
-  kdjData.source = [];
-  cciData.source = [];
-
-  // 获取历史行情
-  queryHistoryDatas({
-    data: {
-      goodscode: props.goodsCode
-    },
-    success: (res) => {
-      const data = res.data;
-      if (data.length) {
-        dataIndex.value = data.length - 1;
-        handleData(data, () => initOptions(updateChartData));
-      } else {
-        showEmpty.value = true;
-      }
-    },
-    complete: () => {
-      loading.value = false;
-    }
-  })
-}
-
-// 指标切换
-const tabChange = (index: number) => {
-  activeSeriesType.value = tabs[index].value;
-  setTimeout(() => {
-    initOptions();
-  }, 0);
-}
-
-// 获取周期毫秒数
-const getCycleMilliseconds = () => {
-  const milliseconds = 60 * 1000; // 一分钟毫秒数
-  switch (props.cycleType) {
-    case EChartsCycleType.minutes5: {
-      return milliseconds * 5;
-    }
-    case EChartsCycleType.minutes30: {
-      return milliseconds * 30;
-    }
-    case EChartsCycleType.minutes60: {
-      return milliseconds * 60;
-    }
-    case EChartsCycleType.hours2: {
-      return milliseconds * 2 * 60;
-    }
-    case EChartsCycleType.Hours4: {
-      return milliseconds * 4 * 60;
-    }
-    case EChartsCycleType.days: {
-      return milliseconds * 24 * 60;
-    }
-    default: {
-      return milliseconds;
-    }
-  }
-}
-
-// 更新图表数据
-const updateChartData = () => {
-  const { source } = klineData,
-    lastIndex = source.length - 1, // 历史行情最后索引位置
-    cycleMilliseconds = getCycleMilliseconds(),
-    newTime = moment(quote.lasttime), // 实时行情最新时间
-    newPrice = quote.last; // 实时行情最新价
-
-  const oldTime = lastIndex === -1 ? newTime : moment(source[lastIndex].date); // 历史行情最后时间
-  const diffTime = newTime.valueOf() - oldTime.valueOf(); // 计算时间差
-
-  if (diffTime > cycleMilliseconds * 2) {
-    // 时间间隔超过两个周期,重新请求历史数据
-  } else {
-    // 判断时间差是否大于周期时间
-    if (lastIndex === -1 || diffTime > cycleMilliseconds) {
-      oldTime.add(cycleMilliseconds, 'ms');
-      const newDate = oldTime.format('YYYY-MM-DD HH:mm:ss');
-
-      // 新增K线数据
-      klineData.source.push({
-        date: newDate,
-        open: newPrice,
-        close: newPrice,
-        lowest: newPrice,
-        highest: newPrice,
-        ma5: '-',
-        ma10: '-',
-        ma15: '-',
-      });
-
-      // 新增MACD数据
-      macdData.source.push({
-        date: newDate,
-        ema12: 0,
-        ema26: 0,
-        dif: 0,
-        dea: 0,
-        macd: 0,
-      })
-
-      // 新增VOL数据
-      volData.source.push({
-        date: newDate,
-        vol: 0,
-      })
-
-      // 新增KDJ数据
-      kdjData.source.push({
-        date: newDate,
-        k: '-',
-        d: '-',
-        j: '-',
-      })
-
-      // 新增CCI数据
-      cciData.source.push({
-        date: newDate,
-        cci: '-',
-      })
-    } else {
-      // 更新列表中最后一条记录的数据
-      const item = source[lastIndex];
-      if (item.lowest > newPrice) {
-        item.lowest = newPrice; // 更新最低价
-      }
-      if (item.highest < newPrice) {
-        item.highest = newPrice; // 更新最高价
-      }
-      item.close = newPrice; // 更新收盘价
-    }
-
-    // 更新各种指标
-    calcIndicator(lastIndex === -1 ? 0 : lastIndex);
-    updateOptions();
-  }
-}
-
-// 监听行情推送
-watch(() => quote.last, () => {
-  if (!loading.value) {
-    updateChartData();
-  }
-})
-
-// 监听周期选择变化
-watch(() => props.cycleType, () => initData());
-</script>
-
-<style lang="less">
-@import './index.less';
-</style>

+ 0 - 71
src/components/modules/echarts-kline/interface.ts

@@ -1,71 +0,0 @@
-/**
- * 图表数据集
- */
-export type Dataset<T> = {
-    dimensions: (keyof T)[],
-    source: T[],
-}
-
-/**
- * K线图表
- */
-export type Candlestick = {
-    date: string, // xAxis数据,必须是第一个属性
-    open: number, // 开盘
-    close: number, // 收盘
-    lowest: number, // 最低
-    highest: number, //最高
-    ma5: string,
-    ma10: string,
-    ma15: string,
-}
-
-export namespace Candlestick {
-    /**
-     * MACD指标
-     */
-    export type MACD = {
-        date: string, // xAxis数据,必须是第一个属性
-        macd: number,
-        dif: number,
-        dea: number,
-        ema12: number,
-        ema26: number,
-    }
-
-    /**
-     * VOL指标
-     */
-    export type VOL = {
-        date: string, // xAxis数据,必须是第一个属性
-        vol: number,
-    }
-
-    /** 
-     * KDJ指标
-     */
-    export type KDJ = {
-        date: string, // xAxis数据,必须是第一个属性
-        k: string,
-        d: string,
-        j: string,
-    }
-
-    /** 
-     * CCI指标
-     */
-    export type CCI = {
-        date: string, // xAxis数据,必须是第一个属性
-        cci: string,
-    }
-
-    /** 
-     * 主题色
-     */
-    export type Colors = {
-        upColor: string,
-        downColor: string,
-        xAxisLineColor: string,
-        yAxisLineColor: string,
-    }
-}

+ 0 - 83
src/components/modules/echarts-timeline/dataset.ts

@@ -1,83 +0,0 @@
-import { TimelineState } from './interface'
-import moment from 'moment';
-
-export function useDataset() {
-    // 行情历史数据中所有补充数据的索引位置(用于指标计算)
-    const invalidData: number[] = [];
-    const state: TimelineState = {
-        rawDate: [],
-        yestClose: 0,
-        decimal: 0,
-        maxMark: 0,
-        minMark: 0,
-        dataset: {
-            dimensions: ['date', 'close', 'ma5'],
-            source: {
-                date: [],
-                close: [],
-                ma5: []
-            },
-        }
-    }
-
-    // 处理行情数据
-    const handleData = (rawData: Ermcp.HistoryDatas[], onReady?: () => void) => {
-        invalidData.length = 0;
-        const { date, close, ma5 } = state.dataset.source
-
-        for (let i = 0; i < rawData.length; i++) {
-            const { ts, c, f } = rawData[i];
-            const time = moment(ts).format('YYYY-MM-DD HH:mm:ss');
-
-            if (f) invalidData.push(i); // 添加补充数据的索引位置
-
-            date.push(time);
-            close.push(c);
-            ma5.push('-');
-        }
-        // 计算各种指标
-        calcIndicator();
-        onReady && onReady();
-    }
-
-    // 计算MA
-    const calcMA = (startIndex = 0) => {
-        const { close, ma5 } = state.dataset.source;
-        for (let i = startIndex; i < close.length; i++) {
-            // 判断是否补充数据
-            if (invalidData.includes(i)) {
-                ma5[i] = i > 0 ? ma5[i - 1].toString() : '-'; // 如果存在,取上一条记录的MA值
-            } else {
-                let n = 0;
-                const tmpList: number[] = [];
-
-                for (let j = i; j >= 0; j--) {
-                    if (invalidData.includes(j)) continue; // 如果是补充数据,跳过本次循环
-                    if (n === 5) break; // 如果 n 等于计数,结束循环
-                    tmpList.push(close[j]);
-                    n++;
-                }
-
-                if (n === 5) {
-                    // 计算总价(收盘价总和)
-                    const total = tmpList.reduce((res, val) => res + val, 0);
-                    // 计算均价
-                    ma5[i] = (total / 5).toFixed(2);
-                } else {
-                    ma5[i] = '-';
-                }
-            }
-        }
-    }
-
-    // 计算各种指标
-    const calcIndicator = (startIndex = 0) => {
-        calcMA(startIndex);
-    }
-
-    return {
-        state,
-        handleData,
-        calcIndicator,
-    }
-}

+ 0 - 116
src/components/modules/echarts-timeline/index.vue

@@ -1,116 +0,0 @@
-<template>
-  <div class="app-echats-timeline">
-    <ul class="legend">
-      <li class="legend-item">收: {{timeDetail ? timeDetail.close : '--'}}</li>
-      <li class="legend-item">MA5: {{timeDetail ? timeDetail.ma5 : '--'}}</li>
-    </ul>
-    <app-echarts :option="timeOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="initData" />
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ref, reactive, watch, computed } from 'vue'
-import { useDataset } from './dataset'
-import { useOptions } from './options'
-import { queryHistoryDatas } from '@/services/api/quote'
-import moment from 'moment';
-import AppEcharts from '@/components/base/echarts/index.vue'
-
-const props = defineProps({
-  goodsCode: String,
-})
-
-const loading = ref(false),
-  showEmpty = ref(false),
-  dataIndex = ref(0); // 当前数据索引值
-
-const { state, handleData, calcIndicator } = useDataset();
-const { timeOption, initOptions } = useOptions(state);
-
-const timeDetail = computed(() => {
-  const { close, ma5 } = state.dataset.source;
-  return {
-    close: close[dataIndex.value],
-    ma5: ma5[dataIndex.value],
-  }
-});
-
-// 模拟最新价推送
-const quote = reactive({
-  last: 0,
-  lasttime: new Date(),
-});
-
-// 初始化数据
-const initData = () => {
-  showEmpty.value = false;
-  loading.value = true;
-  state.dataset.source = {
-    date: [],
-    close: [],
-    ma5: []
-  }
-
-  // 获取历史行情
-  queryHistoryDatas({
-    data: {
-      goodscode: props.goodsCode
-    },
-    success: (res) => {
-      const data = res.data;
-      if (data.length) {
-        dataIndex.value = data.length - 1;
-        handleData(data, () => initOptions(updateChartData));
-      } else {
-        showEmpty.value = true;
-      }
-    },
-    complete: () => {
-      loading.value = false;
-    }
-  })
-}
-
-// 更新图表数据
-const updateChartData = () => {
-  const { date, close, ma5 } = state.dataset.source,
-    lastIndex = close.length - 1, // 历史行情最后索引位置
-    cycleMilliseconds = 60 * 1000, // 一分钟毫秒数
-    newTime = moment(quote.lasttime), // 实时行情最新时间
-    newPrice = quote.last; // 实时行情最新价
-
-  const oldTime = lastIndex === -1 ? newTime : moment(date[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');
-
-      // 新增分时数据
-      date.push(newDate);
-      close.push(newPrice);
-      ma5.push('-')
-    } else {
-      close[lastIndex] = newPrice; // 更新最后一条记录的收盘价
-    }
-
-    // 更新各种指标
-    calcIndicator(lastIndex === -1 ? 0 : lastIndex);
-  }
-}
-
-// 监听行情推送
-watch(() => quote.last, () => {
-  if (!loading.value) {
-    updateChartData();
-  }
-})
-</script>
-
-<style lang="less">
-@import './index.less';
-</style>

+ 0 - 28
src/components/modules/echarts-timeline/interface.ts

@@ -1,28 +0,0 @@
-export type TimelineState = {
-    rawDate: string[], // 原始日期
-    yestClose: number, // 昨日收盘价
-    decimal: number, // 小数位
-    maxMark: number, // Y轴最大刻度
-    minMark: number, // Y轴最小刻度
-    dataset: {
-        dimensions: (keyof Timeline)[],
-        source: Timeline,
-    }
-}
-
-/** 分时线图表 */
-export type Timeline = {
-    date: string[], // xAxis数据,必须是第一个属性
-    close: number[],
-    ma5: string[],
-}
-
-export namespace Timeline {
-    /** 主题色 */
-    export type Colors = {
-        upColor: string,
-        downColor: string,
-        xAxisLineColor: string,
-        yAxisLineColor: string,
-    }
-}

+ 0 - 83
src/components/modules/echarts-timeline/options.ts

@@ -1,83 +0,0 @@
-import { ref, watch } from 'vue'
-import { EChartsOption } from 'echarts'
-import { localCache } from '@/store'
-import { TimelineState, Timeline } from './interface'
-
-export function useOptions(state: TimelineState) {
-    const theme = localCache.getRef('appTheme'),
-        timeOption = ref<EChartsOption>({});
-
-    // 获取图表主题色
-    const getColors = (): Timeline.Colors => {
-        switch (theme.value) {
-            case 'default':
-            case 'dark': {
-                return {
-                    upColor: '#eb5454',
-                    downColor: '#47b262',
-                    xAxisLineColor: '#171B1D',
-                    yAxisLineColor: '#171B1D',
-                };
-            }
-            case 'light': {
-                return {
-                    upColor: '#eb5454',
-                    downColor: '#47b262',
-                    xAxisLineColor: '#DAE5EC',
-                    yAxisLineColor: '#DAE5EC',
-                };
-            }
-        }
-    }
-
-    const initOptions = (onReady?: () => void) => {
-        const { xAxisLineColor } = getColors();
-        timeOption.value = {
-            dataset: state.dataset,
-            xAxis: {
-                type: 'category',
-            },
-            yAxis: {
-                scale: true,
-                splitLine: {
-                    lineStyle: {
-                        // 坐标分隔线颜色
-                        color: xAxisLineColor,
-                    },
-                },
-            },
-            series: [
-                {
-                    name: '分时',
-                    type: 'line',
-                    smooth: true,
-                    symbol: 'none', //中时有小圆点
-                    lineStyle: {
-                        opacity: 0.8,
-                        width: 1,
-                    },
-                },
-                {
-                    name: 'MA5',
-                    type: 'line',
-                    sampling: 'average', //折线图在数据量远大于像素点时候的降采样策略,开启后可以有效的优化图表的绘制效率
-                    smooth: true,
-                    symbol: 'none',
-                    lineStyle: {
-                        width: 1,
-                        opacity: 0.8,
-                    },
-                },
-            ],
-        }
-        onReady && onReady();
-    }
-
-    // 监听主题变化
-    watch(theme, () => initOptions());
-
-    return {
-        timeOption,
-        initOptions,
-    }
-}

+ 80 - 82
src/components/modules/echarts-kline/dataset.ts → src/hooks/echarts/candlestick/dataset.ts

@@ -1,50 +1,51 @@
-import moment from 'moment';
-import { Candlestick, Dataset } from './interface'
+import { reactive } from 'vue'
+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<Candlestick.MACD> = {
-        dimensions: ['date', 'macd', 'dif', 'dea'],
-        source: [],
-    }
-
-    // VOL数据
-    const volData: Dataset<Candlestick.VOL> = {
-        dimensions: ['date', 'vol'],
-        source: [],
-    }
-
-    // KDJ线数据
-    const kdjData: Dataset<Candlestick.KDJ> = {
-        dimensions: ['date', 'k', 'd', 'j'],
-        source: [],
-    }
-
-    // VOL线数据
-    const cciData: Dataset<Candlestick.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: Ermcp.HistoryDatas[], onReady?: () => void) => {
-        invalidData.length = 0;
+    const handleData = (rawData: Ermcp.QueryHistoryDatasRsp[], onReady?: () => void) => {
         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,
@@ -55,7 +56,7 @@ export function useDataset() {
                 ma15: '-',
             })
 
-            macdData.source.push({
+            dataset.macd.source.push({
                 date,
                 ema12: 0,
                 ema26: 0,
@@ -64,19 +65,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: '-',
             })
@@ -87,23 +88,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 = i > 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++;
                 }
 
@@ -111,31 +112,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 Candlestick.MACD, count: number, startIndex = 0) => {
-        type T = Candlestick.MACD[keyof Candlestick.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;
             }
         }
     }
@@ -143,10 +144,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];
@@ -165,21 +166,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天内最低价
@@ -205,22 +206,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
 
@@ -230,23 +231,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,
     }
 }

+ 211 - 0
src/hooks/echarts/candlestick/index.ts

@@ -0,0 +1,211 @@
+import { ref, computed, watch } from 'vue'
+import { EChartsCycleType } from '@/constants/enum/echarts'
+import { queryHistoryDatas } from '@/services/api/quote'
+import { getQuoteDayInfoByCode } from '@/business/common'
+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(false);
+    const dataIndex = ref(-1); // 当前数据索引值
+    const cycleType = ref(EChartsCycleType.minutes);
+    const quote = 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: '--'
+        }
+
+        if (dataIndex.value > -1) {
+            const { open, close, highest, lowest, ma5, ma10, ma15 } = dataset.candlestick.source[dataIndex.value];
+            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: EChartsCycleType) => {
+        clearData();
+        dataIndex.value = -1;
+        cycleType.value = cycletype;
+        isEmpty.value = false;
+        loading.value = true;
+
+        // 获取历史行情
+        queryHistoryDatas({
+            data: {
+                goodscode
+            },
+            success: (res) => {
+                const data = res.data;
+                if (data.length) {
+                    dataIndex.value = data.length - 1;
+                    handleData(data, () => initOptions(updateChart));
+                } else {
+                    isEmpty.value = true;
+                }
+            },
+            complete: () => {
+                loading.value = false;
+            }
+        })
+    }
+
+    /**
+     * 获取周期毫秒数
+     * @returns 
+     */
+    const getCycleMilliseconds = () => {
+        const milliseconds = 60 * 1000; // 一分钟毫秒数
+        switch (cycleType.value) {
+            case EChartsCycleType.minutes5: {
+                return milliseconds * 5;
+            }
+            case EChartsCycleType.minutes30: {
+                return milliseconds * 30;
+            }
+            case EChartsCycleType.minutes60: {
+                return milliseconds * 60;
+            }
+            case EChartsCycleType.hours2: {
+                return milliseconds * 2 * 60;
+            }
+            case EChartsCycleType.Hours4: {
+                return milliseconds * 4 * 60;
+            }
+            case EChartsCycleType.days: {
+                return milliseconds * 24 * 60;
+            }
+            default: {
+                return 0;
+            }
+        }
+    }
+
+    /**
+     * 更新图表数据
+     */
+    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) {
+                // 时间间隔超过两个周期,重新请求历史数据
+            } 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, () => {
+        if (!loading.value) {
+            updateChart();
+        }
+    })
+
+    return {
+        loading,
+        isEmpty,
+        dataIndex,
+        options,
+        selectedItem,
+        initData,
+        initOptions,
+    }
+}

+ 96 - 0
src/hooks/echarts/candlestick/interface.ts

@@ -0,0 +1,96 @@
+
+import { EChartsOption } from 'echarts';
+
+/**
+ * 图表数据集
+ */
+export interface EchartsDataset {
+    invalid: number[]; // 行情历史数据中所有补充数据的索引位置(用于指标计算)
+    candlestick: Dataset<Candlestick>;
+    macd: Dataset<MACD>;
+    vol: Dataset<VOL>;
+    kdj: Dataset<KDJ>;
+    cci: Dataset<CCI>;
+}
+
+/**
+ * 图表数据源
+ */
+export interface Dataset<T> {
+    dimensions: (keyof T)[];
+    source: T[];
+}
+
+/**
+ * K线
+ */
+export type Candlestick = {
+    date: string, // xAxis数据,必须是第一个属性
+    open: number, // 开盘
+    close: number, // 收盘
+    lowest: number, // 最低
+    highest: number, //最高
+    ma5: string,
+    ma10: string,
+    ma15: string,
+}
+
+/**
+ * MACD指标
+ */
+export type MACD = {
+    date: string, // xAxis数据,必须是第一个属性
+    macd: number,
+    dif: number,
+    dea: number,
+    ema12: number,
+    ema26: number,
+}
+
+/**
+ * VOL指标
+ */
+export type VOL = {
+    date: string, // xAxis数据,必须是第一个属性
+    vol: number,
+}
+
+/** 
+ * KDJ指标
+ */
+export type KDJ = {
+    date: string, // xAxis数据,必须是第一个属性
+    k: string,
+    d: string,
+    j: string,
+}
+
+/** 
+ * CCI指标
+ */
+export type CCI = {
+    date: string, // xAxis数据,必须是第一个属性
+    cci: string,
+}
+
+/**
+ * 图表配置项
+ */
+export interface EchartsOptions {
+    colors: Colors;
+    candlestick: EChartsOption;
+    macd: EChartsOption;
+    vol: EChartsOption;
+    kdj: EChartsOption;
+    cci: EChartsOption;
+}
+
+/**
+ * 图表颜色
+ */
+export interface Colors {
+    upColor: string;
+    downColor: string;
+    xAxisLineColor: string;
+    yAxisLineColor: string;
+}

+ 79 - 78
src/components/modules/echarts-kline/options.ts → src/hooks/echarts/candlestick/options.ts

@@ -1,25 +1,54 @@
-import { ref, watch } from 'vue'
+import { reactive, watch } from 'vue'
 import { EChartsOption } from 'echarts';
-import { localCache } from '@/store'
-import { Dataset, Candlestick } from './interface'
+import { EchartsDataset, EchartsOptions, Colors } from './interface'
 import { timerInterceptor } from '@/utils/timer'
-import moment from 'moment';
+import { localCache } from '@/store'
+import moment from 'moment'
 
-export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<Candlestick.MACD>, volData: Dataset<Candlestick.VOL>, kdjData: Dataset<Candlestick.KDJ>, cciData: Dataset<Candlestick.CCI>) {
-    const theme = localCache.getRef('appTheme'),
-        klineOption = ref<EChartsOption>({}),
-        macdOption = ref<EChartsOption>({}),
-        volOption = ref<EChartsOption>({}),
-        kdjOption = ref<EChartsOption>({}),
-        cciOption = ref<EChartsOption>({});
+const theme = localCache.getRef('appTheme');
+
+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 = (): EChartsOption => {
-        const { xAxisLineColor } = getColors();
+        const { source } = dataset.candlestick;
+        const { xAxisLineColor } = options.colors;
+
         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,9 +72,9 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<Ca
     }
 
     // K线配置项
-    const setKlineOption = () => {
-        const { dimensions, source } = klineData;
-        klineOption.value = {
+    const setCandlestickOption = () => {
+        const { dimensions, source } = dataset.candlestick;
+        options.candlestick = {
             ...getDefaultOption(),
             dataset: {
                 dimensions,
@@ -119,9 +148,9 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<Ca
 
     // 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,
@@ -172,8 +201,8 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<Ca
 
     // VOL配置项
     const setVolOption = () => {
-        const { dimensions, source } = volData;
-        volOption.value = {
+        const { dimensions, source } = dataset.vol;
+        options.vol = {
             ...getDefaultOption(),
             dataset: {
                 dimensions,
@@ -192,8 +221,8 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<Ca
 
     // KDJ配置项
     const setKdjOption = () => {
-        const { dimensions, source } = kdjData;
-        kdjOption.value = {
+        const { dimensions, source } = dataset.kdj;
+        options.kdj = {
             ...getDefaultOption(),
             dataset: {
                 dimensions,
@@ -239,8 +268,8 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<Ca
 
     // CCI配置项
     const setCciOption = () => {
-        const { dimensions, source } = cciData;
-        cciOption.value = {
+        const { dimensions, source } = dataset.cci;
+        options.cci = {
             ...getDefaultOption(),
             dataset: {
                 dimensions,
@@ -262,40 +291,22 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<Ca
         }
     }
 
-    // 获取图表主题色
-    const getColors = (): Candlestick.Colors => {
-        switch (theme.value) {
-            case 'default':
-            case 'dark': {
-                return {
-                    upColor: '#eb5454',
-                    downColor: '#47b262',
-                    xAxisLineColor: '#171B1D',
-                    yAxisLineColor: '#171B1D',
-                };
-            }
-            case 'light': {
-                return {
-                    upColor: '#eb5454',
-                    downColor: '#47b262',
-                    xAxisLineColor: '#DAE5EC',
-                    yAxisLineColor: '#DAE5EC',
-                };
-            }
-        }
+    const initOptions = (onReady?: () => void) => {
+        setCandlestickOption();
+        setMacdOption();
+        setVolOption();
+        setKdjOption();
+        setCciOption();
+        onReady && onReady();
     }
 
     // 动态更新数据
     const updateOptions = timerInterceptor.setThrottle(() => {
-        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: [
                 {
@@ -303,53 +314,43 @@ export function useOptions(klineData: Dataset<Candlestick>, macdData: Dataset<Ca
                     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)
 
-    const initOptions = (onReady?: () => void) => {
-        setKlineOption();
-        setMacdOption();
-        setVolOption();
-        setKdjOption();
-        setCciOption();
-        onReady && onReady();
-    }
-
     // 监听主题变化
-    watch(theme, () => initOptions());
+    watch(theme, () => {
+        options.colors = getColors();
+        initOptions();
+    })
 
     return {
-        klineOption,
-        macdOption,
-        volOption,
-        kdjOption,
-        cciOption,
+        options,
         initOptions,
         updateOptions,
     }

+ 146 - 0
src/hooks/echarts/timeline/dataset.ts

@@ -0,0 +1,146 @@
+import { reactive } from 'vue'
+import { getRangeTime } from '@/utils/time'
+import { EchartsDataset } from './interface'
+import moment from 'moment'
+
+export function useDataset() {
+    const dataset = reactive<EchartsDataset>({
+        invalid: [],
+        rawDate: [],
+        yestclose: 0,
+        decimal: 0,
+        maxMark: 0,
+        minMark: 0,
+        interval: 0,
+        timeline: {
+            dimensions: ['date', 'close', 'ma5'],
+            source: {
+                date: [],
+                close: [],
+                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: Ermcp.QueryTSDataRsp, onReady?: () => void) => {
+        const { date, close, ma5 } = dataset.timeline.source;
+        const { preSettle, decimalPlace, runSteps, historyDatas } = rawData;
+
+        dataset.decimal = decimalPlace;
+        dataset.yestclose = preSettle;
+
+        // 开盘交易时间
+        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 < historyDatas.length; i++) {
+            const { ts, c, f } = historyDatas[i];
+            const d = moment(ts).format('YYYY-MM-DD HH:mm:ss');
+
+            if (f) dataset.invalid.push(i); // 添加补充数据的索引位置
+
+            dataset.rawDate.push(d);
+            close.push(c);
+            ma5.push('-');
+        }
+        // 计算各种指标
+        calcIndicator();
+        onReady && onReady();
+    }
+
+    // 计算MA
+    const calcMA = (startIndex = 0) => {
+        const { close, ma5 } = dataset.timeline.source;
+        for (let i = startIndex; i < close.length; 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 (dataset.invalid.includes(j)) continue; // 如果是补充数据,跳过本次循环
+                    if (n === 5) break; // 如果 n 等于计数,结束循环
+                    tmpList.push(close[j]);
+                    n++;
+                }
+
+                if (n === 5) {
+                    // 计算总价(收盘价总和)
+                    const total = tmpList.reduce((res, val) => res + val, 0);
+                    // 计算均价
+                    ma5[i] = (total / 5).toFixed(2);
+                } else {
+                    ma5[i] = '--';
+                }
+            }
+        }
+    }
+
+    // 计算图表最高最低指标线
+    const calcMarkLine = () => {
+        const { close } = 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 = dataset.yestclose - min, // 计算收盘价和最低价的差值
+            b = max - dataset.yestclose; // 计算收盘价和最高价的差值
+
+        // 比较差值大小
+        if (a > b) {
+            max = dataset.yestclose + a;
+            if (last > max) {
+                const c = last - max;
+                max += c;
+                min -= c;
+            }
+        } else {
+            min = min - (b - a);
+            if (min > last) {
+                const c = min - last;
+                max += c;
+                min -= c;
+            }
+        }
+
+        dataset.maxMark = max + point;
+        dataset.minMark = min - point;
+        dataset.interval = (dataset.maxMark - dataset.minMark) / 6;
+    }
+
+    // 计算各项指标
+    const calcIndicator = (startIndex = 0) => {
+        calcMA(startIndex);
+        calcMarkLine();
+    }
+
+    return {
+        dataset,
+        handleData,
+        clearData,
+        calcIndicator,
+    }
+}

+ 106 - 0
src/hooks/echarts/timeline/index.ts

@@ -0,0 +1,106 @@
+import { ref, computed, watch } from 'vue'
+import { queryTSData } from '@/services/api/quote'
+import { getQuoteDayInfoByCode } from '@/business/common'
+import { useDataset } from './dataset'
+import { useOptions } from './options'
+import moment from 'moment';
+
+export function useTimelineChart(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 = 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 = false;
+        loading.value = true;
+
+        // 获取历史行情
+        queryTSData({
+            data: {
+                goodscode
+            },
+            success: (res) => {
+                const { historyDatas } = res.data;
+                if (historyDatas.length) {
+                    dataIndex.value = historyDatas.length - 1;
+                    handleData(res.data, () => initOptions(updateChart));
+                } else {
+                    isEmpty.value = true;
+                }
+            },
+            complete: () => {
+                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) {
+                // 时间间隔超过两个周期,重新请求历史数据
+            } 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, () => {
+        if (!loading.value) {
+            updateChart();
+        }
+    })
+
+    return {
+        loading,
+        isEmpty,
+        dataIndex,
+        options,
+        selectedItem,
+        initData,
+    }
+}

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

@@ -0,0 +1,49 @@
+import { EChartsOption, LinearGradientObject } from 'echarts';
+
+/**
+ * 图表数据集
+ */
+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: EChartsOption;
+}
+
+/**
+ * 图表颜色
+ */
+export interface Colors {
+    upColor: string,
+    downColor: string,
+    xAxisLineColor: string,
+    yAxisLineColor: string,
+    seriesLineColor: string,
+    seriesMarkLabelColor: string,
+    seriesMarkLineColor: string,
+    seriesAreaGradients: LinearGradientObject,
+}

+ 246 - 0
src/hooks/echarts/timeline/options.ts

@@ -0,0 +1,246 @@
+import { reactive, watch } from 'vue'
+import { EchartsDataset, EchartsOptions, Colors } from './interface'
+import { timerInterceptor } from '@/utils/timer'
+import { localCache } from '@/store'
+import * as echarts from 'echarts'
+
+const theme = localCache.getRef('appTheme');
+
+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',
+        }
+    }
+
+    return colors[theme.value];
+}
+
+export function useOptions(dataset: EchartsDataset) {
+    // 图表配置项
+    const options = reactive<EchartsOptions>({
+        colors: getColors(),
+        timeline: {},
+    })
+
+    // 计算涨跌幅百分比,涨跌幅=(今日收盘价-昨日收盘价)/昨日收盘价*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 = (onReady?: () => void) => {
+        const { timeline, maxMark, minMark, interval, yestclose, decimal } = dataset;
+        const { colors } = options;
+
+        options.timeline = {
+            dataset: timeline,
+            axisPointer: {
+                label: {
+                    // 小数点精度
+                    precision: decimal,
+                }
+            },
+            xAxis: {
+                type: 'category',
+                axisLabel: {
+                    showMinLabel: true,
+                    showMaxLabel: true,
+                    margin: 12,
+                },
+            },
+            yAxis: [
+                {
+                    id: 'leftY',
+                    max: maxMark,
+                    min: minMark,
+                    interval,
+                    axisLabel: {
+                        formatter: (val: number) => 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';
+                        },
+                    },
+                    splitLine: {
+                        lineStyle: {
+                            // 坐标分隔线颜色
+                            color: colors.xAxisLineColor,
+                        },
+                    },
+                },
+                {
+                    id: 'rightY',
+                    max: maxMark,
+                    min: minMark,
+                    interval,
+                    axisLabel: {
+                        formatter: (val: number) => calcRatio(val),
+                        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';
+                        },
+                    },
+                    axisPointer: {
+                        show: false,
+                    },
+                    splitLine: {
+                        show: false,
+                    },
+                }
+            ],
+            // series 中不指定 yAxisId 或 yAxisIndex 默认关联 yAxis 第一个配置,xAxis 配置同理
+            series: [
+                {
+                    name: '分时',
+                    type: 'line',
+                    yAxisId: 'leftY',
+                    smooth: true,
+                    symbol: 'none', //中时有小圆点
+                    lineStyle: {
+                        color: colors.seriesLineColor,
+                        opacity: 0.8,
+                        width: 1,
+                    },
+                    areaStyle: {
+                        color: colors.seriesAreaGradients,
+                        shadowColor: 'rgba(0, 0, 0, 0.1)',
+                        shadowBlur: 10,
+                    },
+                    // 标线
+                    markLine: {
+                        // 标线两端图标
+                        symbol: 'none',
+                        // 标线标签样式
+                        label: {
+                            color: colors.seriesMarkLabelColor,
+                            fontWeight: 'bold',
+                            backgroundColor: 'rgba(255,255,255,.75)',
+                            padding: 5,
+                            borderRadius: 3
+                        },
+                        // 标线样式
+                        lineStyle: {
+                            type: 'dashed',
+                        },
+                        data: [
+                            {
+                                yAxis: timeline.source.close[timeline.source.close.length - 1] ?? '--', // 最新价
+                                lineStyle: {
+                                    color: colors.seriesMarkLineColor
+                                },
+                            },
+                            {
+                                yAxis: yestclose, // 昨日收盘价
+                            },
+                        ],
+                    },
+                },
+                {
+                    name: 'MA5',
+                    type: 'line',
+                    sampling: 'average', //折线图在数据量远大于像素点时候的降采样策略,开启后可以有效的优化图表的绘制效率
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+            ],
+        }
+
+        onReady && onReady();
+    }
+
+    // 动态更新数据
+    const updateOptions = timerInterceptor.setThrottle(() => {
+        const { colors } = options;
+        const { timeline, yestclose } = dataset;
+
+        options.timeline = {
+            dataset: {
+                source: timeline.source,
+            },
+            series: [
+                {
+                    name: '分时',
+                    markLine: {
+                        data: [
+                            {
+                                yAxis: timeline.source.close[timeline.source.close.length - 1] ?? '--', // 最新价
+                                lineStyle: {
+                                    color: colors.seriesMarkLineColor
+                                },
+                            },
+                            {
+                                yAxis: yestclose, // 昨日收盘价
+                            },
+                        ],
+                    },
+                },
+            ],
+        }
+    }, 1000)
+
+    // 监听主题变化
+    watch(theme, () => {
+        options.colors = getColors();
+        initOptions();
+    })
+
+    return {
+        options,
+        initOptions,
+        updateOptions,
+    }
+}

+ 3 - 3
src/hooks/theme/index.ts

@@ -1,6 +1,6 @@
 import { localCache } from '@/store'
 import { AppTheme } from '@/constants/enum'
-import h5 from '@/utils/h5plus'
+import plus from '@/utils/h5plus'
 
 /**
  * 系统主题类
@@ -18,7 +18,7 @@ export default new (class {
         const statusBarStyle = AppTheme[theme];
 
         document.documentElement.setAttribute('theme', theme);
-        h5.setStatusBarStyle(statusBarStyle);
+        plus.setStatusBarStyle(statusBarStyle);
         document.removeEventListener('DOMContentLoaded', this.loadTheme);
     }
 
@@ -33,7 +33,7 @@ export default new (class {
         localCache.setValue('appTheme', theme);
 
         if (statusBarStyle) {
-            h5.setStatusBarStyle(statusBarStyle);
+            plus.setStatusBarStyle(statusBarStyle);
         }
     }
 })

+ 1472 - 1
src/mock/quote.ts

@@ -1,5 +1,5 @@
 const historyDatas = {
-    url: '/quote/history',
+    url: '/Quote/QueryHistoryDatas',
     type: 'get',
     response: {
         code: 200,
@@ -3382,6 +3382,1477 @@ const historyDatas = {
     }
 }
 
+const tsData = {
+    url: '/Quote/QueryTSData',
+    type: 'get',
+    response: {
+        code: 200,
+        message: 'success',
+        total: 0,
+        data: {
+            "goodsCode": "SR209",
+            "outGoodsCode": "SR209",
+            "decimalPlace": 0,
+            "tradeDate": "20220523",
+            "startTime": "2022-05-23T09:00:00+08:00",
+            "endTime": "2022-05-23T15:00:00+08:00",
+            "preSettle": 5892,
+            "Count": 225,
+            "historyDatas": [
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:00:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:01:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:02:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:03:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:04:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:05:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:06:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:07:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:08:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:09:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:10:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:11:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:12:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:13:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:14:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:15:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:16:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:17:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:18:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:19:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:20:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:21:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:22:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:23:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:24:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:25:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:26:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:27:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:28:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:29:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:30:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:31:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:32:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:33:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:34:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:35:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:36:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:37:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:38:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:39:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:40:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:41:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:42:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:43:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:44:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:45:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:46:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:47:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5892,
+                    "h": 5892,
+                    "l": 5892,
+                    "c": 5892,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:48:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 170,
+                    "s": 0,
+                    "ts": "2022-05-23T09:49:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:50:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:51:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:52:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:53:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5890,
+                    "h": 5890,
+                    "l": 5890,
+                    "c": 5890,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 170,
+                    "s": 0,
+                    "ts": "2022-05-23T09:54:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5887,
+                    "h": 5887,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 166,
+                    "s": 0,
+                    "ts": "2022-05-23T09:55:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:56:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5882,
+                    "h": 5882,
+                    "l": 5882,
+                    "c": 5882,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 166,
+                    "s": 0,
+                    "ts": "2022-05-23T09:57:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 167,
+                    "s": 0,
+                    "ts": "2022-05-23T09:58:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T09:59:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 168,
+                    "s": 0,
+                    "ts": "2022-05-23T10:00:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5879,
+                    "h": 5879,
+                    "l": 5878,
+                    "c": 5878,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 169,
+                    "s": 0,
+                    "ts": "2022-05-23T10:01:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5879,
+                    "h": 5879,
+                    "l": 5879,
+                    "c": 5879,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 169,
+                    "s": 0,
+                    "ts": "2022-05-23T10:02:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5879,
+                    "h": 5879,
+                    "l": 5879,
+                    "c": 5879,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:03:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5879,
+                    "h": 5879,
+                    "l": 5879,
+                    "c": 5879,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:04:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 171,
+                    "s": 0,
+                    "ts": "2022-05-23T10:05:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5878,
+                    "h": 5878,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 176,
+                    "s": 0,
+                    "ts": "2022-05-23T10:06:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5870,
+                    "h": 5870,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:07:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5870,
+                    "h": 5870,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:08:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5870,
+                    "h": 5870,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 177,
+                    "s": 0,
+                    "ts": "2022-05-23T10:09:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5870,
+                    "h": 5870,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:10:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5927,
+                    "h": 5927,
+                    "l": 5914,
+                    "c": 5914,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 164,
+                    "s": 0,
+                    "ts": "2022-05-23T10:11:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5914,
+                    "h": 5914,
+                    "l": 5914,
+                    "c": 5914,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:12:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5914,
+                    "h": 5914,
+                    "l": 5914,
+                    "c": 5914,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:13:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5914,
+                    "h": 5914,
+                    "l": 5914,
+                    "c": 5914,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:14:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5915,
+                    "h": 5915,
+                    "l": 5915,
+                    "c": 5915,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 165,
+                    "s": 0,
+                    "ts": "2022-05-23T10:15:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5915,
+                    "h": 5915,
+                    "l": 5915,
+                    "c": 5915,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:30:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5907,
+                    "h": 5907,
+                    "l": 5901,
+                    "c": 5901,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 164,
+                    "s": 0,
+                    "ts": "2022-05-23T10:31:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5901,
+                    "h": 5901,
+                    "l": 5901,
+                    "c": 5901,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:32:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5901,
+                    "h": 5901,
+                    "l": 5901,
+                    "c": 5901,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:33:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5901,
+                    "h": 5901,
+                    "l": 5901,
+                    "c": 5901,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:34:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5899,
+                    "h": 5899,
+                    "l": 5899,
+                    "c": 5899,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 164,
+                    "s": 0,
+                    "ts": "2022-05-23T10:35:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5899,
+                    "h": 5899,
+                    "l": 5899,
+                    "c": 5899,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 164,
+                    "s": 0,
+                    "ts": "2022-05-23T10:36:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5899,
+                    "h": 5899,
+                    "l": 5899,
+                    "c": 5899,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:37:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5897,
+                    "h": 5897,
+                    "l": 5896,
+                    "c": 5896,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 165,
+                    "s": 0,
+                    "ts": "2022-05-23T10:38:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5894,
+                    "h": 5894,
+                    "l": 5891,
+                    "c": 5891,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 166,
+                    "s": 0,
+                    "ts": "2022-05-23T10:39:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5889,
+                    "h": 5889,
+                    "l": 5889,
+                    "c": 5889,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 167,
+                    "s": 0,
+                    "ts": "2022-05-23T10:40:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5889,
+                    "h": 5889,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 170,
+                    "s": 0,
+                    "ts": "2022-05-23T10:41:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:42:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:43:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:44:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:45:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:46:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:47:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:48:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:49:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5886,
+                    "h": 5886,
+                    "l": 5886,
+                    "c": 5886,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:50:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5890,
+                    "h": 5890,
+                    "l": 5890,
+                    "c": 5890,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 170,
+                    "s": 0,
+                    "ts": "2022-05-23T10:51:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5887,
+                    "h": 5887,
+                    "l": 5882,
+                    "c": 5882,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 175,
+                    "s": 0,
+                    "ts": "2022-05-23T10:52:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 166,
+                    "s": 0,
+                    "ts": "2022-05-23T10:53:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5882,
+                    "h": 5882,
+                    "l": 5882,
+                    "c": 5882,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 166,
+                    "s": 0,
+                    "ts": "2022-05-23T10:54:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 167,
+                    "s": 0,
+                    "ts": "2022-05-23T10:55:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T10:56:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 168,
+                    "s": 0,
+                    "ts": "2022-05-23T10:57:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5879,
+                    "h": 5879,
+                    "l": 5878,
+                    "c": 5878,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 169,
+                    "s": 0,
+                    "ts": "2022-05-23T10:58:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5879,
+                    "h": 5879,
+                    "l": 5879,
+                    "c": 5879,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 169,
+                    "s": 0,
+                    "ts": "2022-05-23T10:59:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5879,
+                    "h": 5879,
+                    "l": 5879,
+                    "c": 5879,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T11:00:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5879,
+                    "h": 5879,
+                    "l": 5879,
+                    "c": 5879,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T11:01:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5880,
+                    "c": 5880,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 170,
+                    "s": 0,
+                    "ts": "2022-05-23T11:02:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5880,
+                    "h": 5880,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 176,
+                    "s": 0,
+                    "ts": "2022-05-23T11:03:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5870,
+                    "h": 5870,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T11:04:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5870,
+                    "h": 5870,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T11:05:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5870,
+                    "h": 5870,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T11:06:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5870,
+                    "h": 5870,
+                    "l": 5870,
+                    "c": 5870,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 177,
+                    "s": 0,
+                    "ts": "2022-05-23T11:07:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5927,
+                    "h": 5927,
+                    "l": 5914,
+                    "c": 5914,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 164,
+                    "s": 0,
+                    "ts": "2022-05-23T11:08:00+08:00",
+                    "f": false
+                },
+                {
+                    "o": 5914,
+                    "h": 5914,
+                    "l": 5914,
+                    "c": 5914,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T11:09:00+08:00",
+                    "f": true
+                },
+                {
+                    "o": 5914,
+                    "h": 5914,
+                    "l": 5914,
+                    "c": 5914,
+                    "tv": 0,
+                    "tt": 0,
+                    "hv": 0,
+                    "s": 0,
+                    "ts": "2022-05-23T11:10:00+08:00",
+                    "f": true
+                }
+            ],
+            "runSteps": [
+                {
+                    "groupid": 1,
+                    "tradeweekday": 1,
+                    "sectionid": 1,
+                    "runstep": 2,
+                    "startweekday": 1,
+                    "starttime": "09:00",
+                    "endweekday": 1,
+                    "endtime": "10:15",
+                    "startflag": 0,
+                    "endflag": 0,
+                    "start": "2022-05-23T09:00:00+08:00",
+                    "end": "2022-05-23T10:15:00+08:00"
+                },
+                {
+                    "groupid": 1,
+                    "tradeweekday": 1,
+                    "sectionid": 2,
+                    "runstep": 2,
+                    "startweekday": 1,
+                    "starttime": "10:30",
+                    "endweekday": 1,
+                    "endtime": "11:30",
+                    "startflag": 0,
+                    "endflag": 0,
+                    "start": "2022-05-23T10:30:00+08:00",
+                    "end": "2022-05-23T11:30:00+08:00"
+                },
+                {
+                    "groupid": 1,
+                    "tradeweekday": 1,
+                    "sectionid": 3,
+                    "runstep": 2,
+                    "startweekday": 1,
+                    "starttime": "13:30",
+                    "endweekday": 1,
+                    "endtime": "15:00",
+                    "startflag": 0,
+                    "endflag": 0,
+                    "start": "2022-05-23T13:30:00+08:00",
+                    "end": "2022-05-23T15:00:00+08:00"
+                }
+            ]
+        }
+    }
+}
+
 export default [
     historyDatas,
+    tsData,
 ]

+ 2 - 2
src/packages/mobile/components/layouts/statusbar/index.vue

@@ -6,7 +6,7 @@
 
 <script lang="ts" setup>
 import { ref, onMounted, nextTick } from 'vue'
-import h5 from '@/utils/h5plus'
+import plus from '@/utils/h5plus'
 
 const emit = defineEmits(['ready']);
 
@@ -16,7 +16,7 @@ const statusbarElement = ref<HTMLElement>();
 onMounted(() => {
   const el = statusbarElement.value;
 
-  h5.getStatusBarHeight((height) => {
+  plus.getStatusBarHeight((height) => {
     el?.style.setProperty('padding-top', height + 'px');
   })
 

+ 2 - 7
src/packages/mobile/views/account/login/index.vue

@@ -8,7 +8,7 @@
           <Field v-model="account.LoginID" name="account" label="用户名" placeholder="请输入用户名"
             :rules="[{ required: true, message: '随便输入' }]" />
           <Field v-model="account.LoginPWD" name="password" type="password" label="密码" placeholder="请输入密码"
-            :rules="[{ required: true, message: '随便输入' }]" autocomplete />
+            :rules="[{ required: true, message: '随便输入' }]" autocomplete="on" />
         </CellGroup>
         <div class="login-button">
           <Button type="primary" :loading="loading" loading-text="正在登录..." native-type="submit" round block>登录</Button>
@@ -30,12 +30,7 @@ const { historyStack } = animateRouter.getState();
 const showBackButton = computed(() => historyStack.value.length > 1);
 
 const loginAction = () => {
-  userLogin((msg) => {
-    notify({
-      type: msg ? 'danger' : 'success',
-      message: msg ?? '登录成功',
-    })
-  })
+  userLogin().then(() => notify('登录成功', 'success')).catch((err) => notify(err, 'danger'));
 }
 </script>
 

+ 3 - 3
src/packages/mobile/views/boot/index.vue

@@ -28,7 +28,7 @@ import { reactive } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { Circle, Button, Loading } from 'vant'
 import service from '@/services'
-import h5 from '@/utils/h5plus'
+import plus from '@/utils/h5plus'
 
 const route = useRoute();
 const router = useRouter();
@@ -61,7 +61,7 @@ const timer = window.setInterval(() => {
 // 跳过广告
 const skip = () => {
   clearInterval(timer);
-  h5.exitFullSreen();
+  plus.exitFullSreen();
 
   const redirect = route.query.redirect;
   if (redirect) {
@@ -71,7 +71,7 @@ const skip = () => {
   }
 }
 
-h5.setFullSreen();
+plus.setFullSreen();
 initService();
 </script>
 

+ 0 - 0
src/components/modules/echarts-kline/index.less → src/packages/pc/components/modules/echarts-kline/index.less


+ 120 - 0
src/packages/pc/components/modules/echarts-kline/index.vue

@@ -0,0 +1,120 @@
+<template>
+  <div class="app-echats-kline">
+    <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" :empty="isEmpty" v-model:index="dataIndex" v-model:loading="loading"
+        @ready="mainReady" />
+    </div>
+    <template v-if="showIndicator">
+      <div class="app-echats-kline__container indicator">
+        <!-- MACD -->
+        <section class="section" v-if="activeSeriesType === EChartsSeriesType.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" :empty="isEmpty" v-model:index="dataIndex" v-model:loading="loading"
+            @ready="indicatorReady" />
+        </section>
+        <!-- VOL -->
+        <section class="section" v-if="activeSeriesType === EChartsSeriesType.VOL">
+          <ul class="legend">
+            <li class="legend-item">VOL: {{ selectedItem.vol }}</li>
+          </ul>
+          <app-echarts :option="options.vol" :empty="isEmpty" v-model:index="dataIndex" v-model:loading="loading"
+            @ready="indicatorReady" />
+        </section>
+        <!-- KDJ -->
+        <section class="section" v-if="activeSeriesType === EChartsSeriesType.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" :empty="isEmpty" v-model:index="dataIndex" v-model:loading="loading"
+            @ready="indicatorReady" />
+        </section>
+        <!-- CCI -->
+        <section class="section" v-if="activeSeriesType === EChartsSeriesType.CCI">
+          <ul class="legend">
+            <li class="legend-item">CCI: {{ selectedItem.cci }}</li>
+          </ul>
+          <app-echarts :option="options.cci" :empty="isEmpty" v-model:index="dataIndex" v-model:loading="loading"
+            @ready="indicatorReady" />
+        </section>
+      </div>
+      <app-tab theme="menu" :data-source="tabs" @change="tabChange" />
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, PropType, watch } from 'vue'
+import { EChartsCycleType, EChartsSeriesType } from '@/constants/enum'
+import { useCandlestickChart } from '@/hooks/echarts/candlestick'
+import AppEcharts from '@/components/base/echarts/index.vue'
+import AppTab from '@/components/base/tab/index.vue'
+import * as echarts from 'echarts'
+
+const props = defineProps({
+  goodscode: {
+    type: String,
+    default: '',
+  },
+  // 周期类型
+  cycleType: {
+    type: Number as PropType<EChartsCycleType>,
+    default: EChartsCycleType.minutes,
+  },
+  // 是否显示指标
+  showIndicator: {
+    type: Boolean,
+    default: true,
+  },
+})
+
+const { loading, dataIndex, isEmpty, options, selectedItem, initData, initOptions } = useCandlestickChart(props.goodscode);
+const activeSeriesType = ref(EChartsSeriesType.MACD); // 当前选中的指标
+const chartGroup = new Map<string, echarts.ECharts>(); // 图表联动实例组
+
+const tabs = [
+  { label: 'MACD', value: EChartsSeriesType.MACD },
+  { label: 'VOL', value: EChartsSeriesType.VOL },
+  { label: 'KDJ', value: EChartsSeriesType.KDJ },
+  { label: 'CCI', value: EChartsSeriesType.CCI },
+]
+
+// 指标切换
+const tabChange = (index: number) => {
+  activeSeriesType.value = tabs[index].value;
+  setTimeout(() => {
+    initOptions();
+  }, 0);
+}
+
+const mainReady = (chart: echarts.ECharts) => {
+  chartGroup.set('main', chart);
+  initData(props.cycleType);
+}
+
+const indicatorReady = (chart: echarts.ECharts) => {
+  chartGroup.set('indicator', chart);
+  echarts.connect([...chartGroup.values()]); // 图表联动
+}
+
+// 监听周期选择变化
+watch(() => props.cycleType, (val) => initData(val));
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 0 - 0
src/components/modules/echarts-timeline/index.less → src/packages/pc/components/modules/echarts-timeline/index.less


+ 28 - 0
src/packages/pc/components/modules/echarts-timeline/index.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="app-echats-timeline">
+    <ul class="legend">
+      <li class="legend-item">收: {{ selectedItem.close }}</li>
+      <li class="legend-item">MA5: {{ selectedItem.ma5 }}</li>
+    </ul>
+    <app-echarts :option="options.timeline" :empty="isEmpty" v-model:index="dataIndex" v-model:loading="loading"
+      @ready="initData" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useTimelineChart } from '@/hooks/echarts/timeline'
+import AppEcharts from '@/components/base/echarts/index.vue'
+
+const props = defineProps({
+  goodscode: {
+    type: String,
+    default: '',
+  },
+})
+
+const { loading, dataIndex, isEmpty, options, selectedItem, initData } = useTimelineChart(props.goodscode);
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 8 - 9
src/packages/pc/views/account/login/index.vue

@@ -1,7 +1,8 @@
 <template>
   <account-layout class="account-login" title="账号登录">
     <div class="form-input">
-      <el-input placeholder="用户名/账号/手机号" maxlength="20" v-model="account.LoginID" v-rules="[{ required: true, message: '随便输入' }]"></el-input>
+      <el-input placeholder="用户名/账号/手机号" maxlength="20" v-model="account.LoginID"
+        v-rules="[{ required: true, message: '随便输入' }]"></el-input>
     </div>
     <div class="form-input">
       <el-input type="password" placeholder="请输入您的登录密码" v-model="account.LoginPWD" v-rules="rules.LoginPWD"></el-input>
@@ -40,14 +41,12 @@ const rules: FormRules<Proto.LoginReq> = {
 
 const loginAction = (message: string[]) => {
   if (!message.length) {
-    userLogin((msg) => {
-      if (msg) {
-        ElMessage({
-          type: 'error',
-          message: msg,
-          showClose: true,
-        })
-      }
+    userLogin().catch((err) => {
+      ElMessage({
+        type: 'error',
+        message: err,
+        showClose: true,
+      })
     })
   }
 }

+ 2 - 2
src/packages/pc/views/market/goods/index.vue

@@ -16,8 +16,8 @@ import { ref } from 'vue'
 import { timerInterceptor } from '@/utils/timer'
 import { globalState } from '@/store'
 import { EChartsCycleType } from '@/constants/enum'
-import AppEchartsKline from '@/components/modules/echarts-kline/index.vue'
-import AppEchartsTimeline from '@/components/modules/echarts-timeline/index.vue'
+import AppEchartsKline from '@pc/components/modules/echarts-kline/index.vue'
+import AppEchartsTimeline from '@pc/components/modules/echarts-timeline/index.vue'
 import AppTab from '@/components/base/tab/index.vue'
 
 const scrollEl = ref<HTMLDivElement>();

+ 9 - 2
src/services/api/quote/index.ts

@@ -4,6 +4,13 @@ import { HttpRequest } from '@/services/http/interface'
 /**
  * 历史行情
  */
-export function queryHistoryDatas(params: HttpRequest<{ goodscode?: string }, Ermcp.HistoryDatas[]>) {
-    return httpRequest('/quote/history', 'get', params);
+export function queryHistoryDatas(params: HttpRequest<{ goodscode?: string }, Ermcp.QueryHistoryDatasRsp[]>) {
+    return httpRequest('/Quote/QueryHistoryDatas', 'get', params);
+}
+
+/**
+ * 历史行情
+ */
+export function queryTSData(params: HttpRequest<{ goodscode?: string }, Ermcp.QueryTSDataRsp>) {
+    return httpRequest('/Quote/QueryTSData', 'get', params);
 }

+ 1 - 1
src/services/socket/index.ts

@@ -32,7 +32,7 @@ export default new (class {
         }
 
         this.tradeServer.onPush = ({ funCode }) => {
-            const delay = 2000; // 延迟推送消息,防止短时间内重复请求
+            const delay = 1000; // 延迟推送消息,防止短时间内重复请求
 
             switch (funCode) {
                 case FunCode.MoneyChangedNotify: {

+ 4 - 5
src/services/socket/trade/protobuf/index.ts

@@ -1,15 +1,14 @@
-import Protobuf from 'protobufjs';
+import Protobuf from 'protobufjs'
 
 export default new (class {
     private readonly protoRoot: Promise<unknown>;
 
     /**
      * 加载proto文件
-     * @param proto proto文件地址
      */
-    constructor(proto: string) {
+    constructor() {
         this.protoRoot = new Promise((resolve, reject) => {
-            Protobuf.load(proto, (err: Error, root: unknown) => {
+            Protobuf.load('./proto/mtp.proto', (err: Error, root: unknown) => {
                 if (err) {
                     reject(err)
                 } else {
@@ -57,4 +56,4 @@ export default new (class {
         const build = await this.build(protoName);
         return build.decode(content);
     }
-})('./proto/mtp.proto')
+})

+ 33 - 2
src/types/ermcp/quote.d.ts

@@ -127,8 +127,8 @@ declare namespace Ermcp {
         utclasttime: string;
     }
 
-    /** 历史行情 */
-    interface HistoryDatas {
+    /** K线历史数据 */
+    interface QueryHistoryDatasRsp {
         o: number;
         h: number;
         l: number;
@@ -140,4 +140,35 @@ declare namespace Ermcp {
         ts: string;
         f: boolean;
     }
+
+    /** 分时历史数据 */
+    interface QueryTSDataRsp {
+        Count: number;
+        decimalPlace: number;
+        endTime: string;
+        goodsCode: string;
+        outGoodsCode: string;
+        preSettle: number;
+        startTime: string;
+        tradeDate: string;
+        historyDatas: QueryHistoryDatasRsp[];
+        runSteps: QueryTSDataRsp.RunSteps[];
+    }
+
+    namespace QueryTSDataRsp {
+        interface RunSteps {
+            end: string;
+            endflag: number;
+            endtime: string;
+            endweekday: number;
+            groupid: number;
+            runstep: number;
+            sectionid: number;
+            start: string;
+            startflag: number;
+            starttime: string;
+            startweekday: number;
+            tradeweekday: number;
+        }
+    }
 }

+ 34 - 0
src/utils/time/index.ts

@@ -0,0 +1,34 @@
+import moment, { DurationInputArg1, Moment, unitOfTime } from 'moment'
+
+/**
+ * 获取两个时间范围内的所有时间
+ * @param t1 时间1
+ * @param t2 时间2
+ * @param format 时间格式化类型
+ * @param unit 时间单位
+ * @param amount 时间区间间隔数
+ * @returns
+ */
+export function getRangeTime(t1: string | Moment | Date, t2: string | Moment | Date, format = 'YYYYMMDD', unit: unitOfTime.DurationConstructor = 'd', amount: DurationInputArg1 = 1): string[] {
+    const fn = (val: Moment) => val.format(format);
+    // 处理开始时间和结束时间
+    let startTime = moment(t1);
+    let endTime = moment(t2);
+
+    const result: string[] = [];
+    const isSame = () => startTime.isSame(endTime, unit);
+
+    if (isSame()) {
+        return [fn(startTime), fn(startTime)];
+    } else {
+        if (startTime.isAfter(endTime)) {
+            [startTime, endTime] = [endTime, startTime]
+        }
+        while (!isSame()) {
+            result.push(fn(startTime));
+            startTime = startTime.add(amount, unit);
+        }
+        result.push(fn(endTime));
+        return result;
+    }
+}

+ 10 - 8
src/utils/vant/index.ts

@@ -1,17 +1,19 @@
-import { Notify, NotifyOptions } from 'vant'
+import { Notify, NotifyType } from 'vant'
 import { timerInterceptor } from '@/utils/timer'
-import h5 from '@/utils/h5plus'
+import plus from '@/utils/h5plus'
 
 /**
  * 消息通知
- * @param options 
+ * @param message 
+ * @param type 
+ * @param duration 
  */
-export function notify(options: NotifyOptions) {
-    const duration = options.duration ?? 2000;
-    h5.hideStatusBar();
-    Notify({ ...options, duration });
+export function notify(message?: string, type?: NotifyType, duration = 2000) {
+    plus.hideStatusBar();
+
+    Notify({ type, message, duration });
 
     timerInterceptor.debounce(() => {
-        h5.showStatusBar();
+        plus.showStatusBar();
     }, duration, 'notify')
 }