vue3+echarts+websocket分时图与K线图实时推送

news/2024/12/15 23:52:52/

一、父组件代码:

<template>

  <div class="chart-box" v-loading="loading">

    <!-- tab导航栏 -->

    <div class="tab-box">

      <div class="tab-list">

        <div

          v-for="(item, index) in tabList"

          :key="index"

          class="item-tab"

          @click="handleClick(index)"

        >

          <div :class="tabActive === index ? 'color' : ''" class="tab">

            {{ item }}

          </div>

          <div v-if="tabActive === index" class="line-box" />

        </div>

      </div>

    </div>

    <!-- k线图板块 -->

    <div class="Kchart-box" v-if="tabActive === 0">

      <!-- 导航栏按钮 -->

      <div class="btn-options">

        <div

          class="btn"

          v-for="(item, index) in groupList"

          :key="index"

          :class="activeIndex === index ? 'color' : ''"

          @click="onClickItem(item, index)"

        >

          {{ item.name }}

        </div>

      </div>

      <!-- k线板块 -->

      <div class="kChart">

        <div :style="{ width: '100%' }" class="chart">

          <!-- 分时图 -->

          <chartMin

            v-if="activeIndex === 0"

            :pre-price="prePrice"

            :data-list="list"

            :minDateList1="minDateList1"

            :digit="digit"

            :current-index="activeIndex"

            class="chartMin"

          >

          </chartMin>

          <!-- k线图 -->

          <chartK

            v-if="activeIndex !== 0"

            :data-list="listK"

            :digit="digit"

            :current-tab="activeIndex"

            :current-index="currentIndex"

            class="chartMin"

            @getHoverData="getHoverData"

          >

          </chartK>

          <div v-if="activeIndex !== 0" class="indexBtn">

            <span

              :class="{ active: currentIndex === 1 }"

              @click="choseIndex(1)"

            >

              成交量

            </span>

            <span

              :class="{ active: currentIndex === 2 }"

              @click="choseIndex(2)"

            >

              MACD

            </span>

            <span

              :class="{ active: currentIndex === 3 }"

              @click="choseIndex(3)"

            >

              KDJ

            </span>

            <span

              :class="{ active: currentIndex === 4 }"

              @click="choseIndex(4)"

            >

              RSI

            </span>

          </div>

          <div

            v-if="activeIndex !== 0 && currentIndex === 1"

            class="pos-box macd-box"

          >

            <p>

              成交量(手):

              <span>{{

                KHoverData[5] == null ? '' : formatNumUnit(KHoverData[5])

              }}</span>

            </p>

          </div>

          <div

            v-if="activeIndex !== 0 && currentIndex === 2"

            class="pos-box macd-box"

          >

            <p>

              MACD:

              <span>{{ KHoverData[8] }}</span

              >&nbsp;&nbsp; <span class="color1"> DEA:</span>

              <span>{{ KHoverData[9] }}</span

              >&nbsp;&nbsp; <span class="color2"> DIF:</span>

              <span>{{ KHoverData[10] }}</span

              >&nbsp;&nbsp;

            </p>

          </div>

          <div

            v-if="activeIndex !== 0 && currentIndex === 3"

            class="pos-box macd-box"

          >

            <p>

              <span class="color1">K:</span>

              <span>{{ KHoverData[13] }}</span

              >&nbsp;&nbsp; <span class="color2">D:</span>

              <span>{{ KHoverData[11] }}</span

              >&nbsp;&nbsp; <span class="color3">J:</span>

              <span>{{ KHoverData[12] }}</span

              >&nbsp;&nbsp;

            </p>

          </div>

          <div

            v-if="activeIndex !== 0 && currentIndex === 4"

            class="pos-box macd-box"

          >

            <p>

              <span class="color1">RSI6:</span>

              <span>{{ KHoverData[14] }}</span

              >&nbsp;&nbsp; <span class="color2">RSI12:</span>

              <span>{{ KHoverData[15] }}</span

              >&nbsp;&nbsp; <span class="color3">RSI24:</span>

              <span>{{ KHoverData[16] }}</span

              >&nbsp;&nbsp;

            </p>

          </div>

        </div>

      </div>

    </div>

  </div>

</template>

<script setup lang="ts">

import { ElMessage } from 'element-plus'

import chartMin from './chartMin.vue'

import chartK from './chartk.vue'

import common from '@/utils/common'

import useWebSocket from '@/utils/useWebSocket'

import { WEBSOCKET_URL } from '@/service/config'

import { queryMinDate } from '@/service/stockIndex/index'

const props = defineProps({

  securityId: {

    // 证券id

    type: [String, Number],

    required: true

  },

  symbol: {

    // 证券代码

    type: String,

    default: ''

  },

  market: {

    // 证券市场

    type: String,

    default: ''

  },

  tagIndex: {

    // tab索引

    type: Number,

    default: null

  }

})

const emit = defineEmits(['getKLineType'])

const minChartList = ref<any>([]) // 分时图行情数据

const minDateList1 = ref<any>([]) // 分时图行情数据

const kChartList = ref<any>([]) // k线图行情数据

const prePrice = ref<any>() // 昨收价

const digit = ref(2) // 小数位

const list = ref<any>([]) // 分时图数据

const minDateList = ref<any>([]) // 分时图时间段

const kDateList = ref<any>([]) // K线图时间段

const listK = ref<any>([]) // k线图数据

const loading = ref(false) // 加载状态

const activeIndex = ref(0) // 当前选择的K线图tab

const tabActive = ref(0) // 当前选择的顶部tab

const currentIndex = ref(1) // 当前选择的指标

const KHoverData = ref<any>([]) // k线hoverdata

const dateType = ref<any>(60) // 获取时间段类型值

const KlineStock = ref() // K线图websocket实例

const securityId1 = ref(props.securityId) // 证券id

const market1 = ref<any>(props.market) // 证券市场

const symbol1 = ref<any>(props.symbol) // 证券代码

const tabList = [

  // 导航栏数据

  'K线图'

]

const groupList = [

  {

    id: 60,

    name: '分时图'

  },

  {

    id: 1,

    name: '日K线'

  },

  {

    id: 4,

    name: '周K线'

  },

  {

    id: 7,

    name: '月K线'

  },

  {

    id: 300,

    name: '5分钟'

  },

  {

    id: 1800,

    name: '30分钟'

  },

  {

    id: 3600,

    name: '60分钟'

  }

]

//监听参数值重新渲染数据

watch(

  () => [props.securityId, props.market, props.symbol],

  (newVal, oldVal) => {

    if (newVal[0] !== oldVal[0]) {

      securityId1.value = newVal[0]

    }

    if (newVal[1] !== oldVal[1]) {

      market1.value = newVal[1]

    }

    if (newVal[2] !== oldVal[2]) {

      symbol1.value = newVal[2]

    }

    minChartList.value = []

    minDateList.value = []

    kChartList.value = []

    KHoverData.value = []

    list.value = []

    listK.value = []

    tabActive.value = 0

    activeIndex.value = 0

    currentIndex.value = 1

    dateType.value = 60

    getMinDate(securityId1.value, dateType.value)

    // 关闭连接

    closeAllSocket()

    // 重新建立连接

    webSocketInit()

  },

  { deep: true }

)

//初始化websocket

const webSocketInit = () => {

  KlineStock.value = useWebSocket({

    url: `${WEBSOCKET_URL}/api/web_socket/QuotationHub/Subscribe/${

      market1.value

    }/${securityId1.value}/${symbol1.value}/${dateType.value}`,

    heartBeatData: ''

  })

  KlineStock.value.connect()

}

//监听分时图与K线图websocket数据推送变更

watch(

  () => KlineStock.value && KlineStock.value.message,

  (res: any) => {

    if (res && res.code === 200 && res.data) {

      if (activeIndex.value === 0) {

        // 判断分时图推送数据是否大于1,大于1为历史数据,否则为最新推送数据

        if (JSON.parse(res.data).length > 1) {

          JSON.parse(res.data).forEach((el: any) => {

            // 判断数据是否存在分时图数据中

            const flag = minChartList.value.some(

              (el1: any) => el1.KData.UT === el.KData.UT

            )

            if (!flag) {

              // 不存在则push

              minChartList.value.push(el)

            }

          })

        } else {

          // 获取时间x轴上推送过来的时间点的下标

          let i = minDateList1.value.indexOf(JSON.parse(res.data)[0].KData.UT)

          if (i > -1) {

            // 如果时间段小于或等于当前下标则直接push

            if (minChartList.value.length <= i) {

              minChartList.value.push(JSON.parse(res.data)[0])

            } else {

              // 如果大于则清空时间段直接赋值

              minChartList.value[i] = JSON.parse(res.data)[0]

              for (let j = i + 1; j < minChartList.value.length; j++) {

                minChartList.value[j] = []

              }

            }

          }

        }

        refreshMinChart(minChartList.value)

      } else {

        // 判断K线图推送数据是否大于1,大于1为历史数据,否则为最新推送数据

        if (JSON.parse(res.data).length > 1) {

          JSON.parse(res.data).forEach((el: any) => {

            // 判断数据是否存在K线图数据中

            const flag1 = kChartList.value.some(

              (el1: any) => el1.KData.UT === el.KData.UT

            )

            if (!flag1) {

              // 不存在则push

              kChartList.value.push(el)

            }

          })

        } else {

          // 取最新数据的最后一条数据

          const arr = kChartList.value[kChartList.value.length - 1]

          // 判断时间是否相等

          if (arr.KData && arr.KData.UT === JSON.parse(res.data)[0].KData.UT) {

            // 相等则删除最后一条,更新新的一条进去

            kChartList.value.pop()

            kChartList.value.push(...JSON.parse(res.data))

          } else {

            // 不相等则直接push

            kChartList.value.push(JSON.parse(res.data)[0])

          }

        }

        refreshKChart()

      }

    }

  }

)

// 顶部tab栏切换点击

const handleClick = (index: number) => {

  tabActive.value = index

  if (tabActive.value === 0) {

    dateType.value = 60

    emit('getKLineType', dateType.value)

    getMinDate(props.securityId, dateType.value)

    minChartList.value = []

    kChartList.value = []

    KHoverData.value = []

    // 关闭连接

    closeAllSocket()

    // 重新建立连接

    webSocketInit()

  }

}

// K线图tab栏切换

const onClickItem = (item: any, index: number) => {

  dateType.value = item.id

  activeIndex.value = index

  emit('getKLineType', dateType.value)

  getMinDate(props.securityId, dateType.value)

  minChartList.value = []

  kChartList.value = []

  KHoverData.value = []

  // 关闭连接

  closeAllSocket()

  // 重新建立连接

  webSocketInit()

}

// 获取分时图时间段

const getMinDate = (securityId: any, type: number) => {

  loading.value = true

  securityId = securityId1.value

  type = dateType.value

  minDateList.value = []

  kDateList.value = []

  queryMinDate(securityId, type).then((res: any) => {

    if (res.code === 200) {

      minDateList1.value = res.data

      // 数据处理(把每一项字符串转成数组字符串,便于后面行情数据处理—)

      res.data.map((r: any) => {

        const item = r.split()

        if (activeIndex.value === 0) {

          minDateList.value.push(toRaw(item))

        } else {

          kDateList.value.push(toRaw(item))

        }

      })

    } else {

      ElMessage({

        message: res.message,

        type: 'error'

      })

    }

    loading.value = false

  })

}

// 刷新分时图

const refreshMinChart = (data: any) => {

  // 获取L1Min分时行情

  let lstData: any[] = []

  // 折线数据[utc,cp,cr,pp,avg,ta,tv]

  data.forEach((element: any) => {

    const item = [

      element.KData.UT, // 时间

      element.KData.CP, // 最新价

      element.KData.Avg, // 均价

      element.KData.TV, // 总量

      element.KData.TA, // 总额

      element.KData.CR, // 涨跌幅

      element.KData.PP // 昨收

    ]

    lstData.push(item)

  })

  list.value = lstData

  prePrice.value = list.value[0][6] // 获取昨收价确定均线位置

}

// 刷新K线图

const refreshKChart = () => {

  let lstKData: any[] = []

  // 折线数据

  kChartList.value.forEach((element: any) => {

    const item = [

      element.KData.UT,

      element.KData.OP, // 开盘值

      element.KData.CP, // 收盘值

      element.KData.LP, // 最低值

      element.KData.HP, // 最高值

      element.KData.TV, // 总量

      element.KData.TA, // 总额

      element.KData.CR, // 涨跌幅

      element.KIndex.MACD, // mace

      element.KIndex.DEA, // dea

      element.KIndex.DIF, // dif

      element.KIndex.D, // d

      element.KIndex.J, // j

      element.KIndex.K, // k

      element.KIndex.RSI6, // RSI6

      element.KIndex.RSI12, // RSI12

      element.KIndex.RSI24, // RSI24

      element.KData.CG //涨跌

    ]

    lstKData.push(item)

  })

  listK.value = lstKData

}

// 获取k线数据

const getHoverData = (data: any) => {

  KHoverData.value = data

}

// 切换指标

const choseIndex = (index: number) => {

  currentIndex.value = index

  KHoverData.value = []

}

// 大数字单位处理(小于10万不处理)

const formatNumUnit = (value: any) => {

  return common.formatNumUnit(value)

}

const closeAllSocket = () => {

  //断开全部websocket连接

  KlineStock.value && KlineStock.value.disconnect()

}

onMounted(() => {

  getMinDate(securityId1.value, dateType.value)

  //当前页面刷新清空

  closeAllSocket()

  webSocketInit()

})

onUnmounted(() => {

  closeAllSocket()

})

</script>

<style lang="less" scoped>

.chart-box {

  .tab-box {

    width: 100%;

    display: flex;

    background-color: #ffffff;

    margin-top: 12px;

    margin-bottom: 4px;

    .tab-list {

      height: 100%;

      display: flex;

      .item-tab {

        height: 100%;

        padding: 0 20px;

        display: flex;

        justify-content: center;

        align-items: center;

        flex-direction: column;

        cursor: pointer;

        position: relative;

        &:first-child {

          padding-left: 0;

        }

        .tab {

          font-weight: normal;

          font-size: 14px;

          color: #666666;

          position: relative;

        }

        .color {

          color: #3a5bb7;

          font-weight: 600;

        }

        .line-box {

          width: 40px;

          height: 3px;

          background: #3a5bb7;

          position: absolute;

          bottom: -9px;

          border-radius: 2px 2px 0px 0px;

        }

      }

    }

  }

  .btn-options {

    display: flex;

    margin: 25px 0 5px;

    .btn {

      padding: 0 15px;

      height: 24px;

      background: #f4f7fc;

      border-radius: 6px;

      font-weight: 400;

      font-size: 13px;

      color: #999999;

      display: flex;

      align-items: center;

      justify-content: center;

      margin-right: 14px;

      border: 1px solid #f4f7fc;

      cursor: pointer;

      &:hover {

        color: #3a5bb7;

      }

    }

    .color {

      color: #3a5bb7;

      border: 1px solid #3a5bb7;

      font-weight: 500;

      background-color: #ffffff;

    }

  }

  .chart {

    width: 100%;

    height: 360px;

    margin-bottom: 16px;

    position: relative;

    .chartMin {

      width: 100%;

      height: 100%;

    }

    .indexBtn {

      width: 100%;

      position: absolute;

      left: 8%;

      top: 83.8%;

      height: 38px;

      span {

        width: 21%;

        text-align: center;

        display: inline-block;

        line-height: 25px;

        height: 25px;

        border: 1px solid #3a5bb7;

        color: #3a5bb7;

        border-right: none;

      }

      span:last-child {

        border-right: 1px solid #3a5bb7;

      }

      span:hover,

      .active {

        cursor: pointer;

        color: #fff;

        background: #3a5bb7;

      }

    }

    .pos-box {

      position: absolute;

    }

    .macd-box {

      top: 51.5%;

      left: 8%;

      color: #666666;

      font-size: 12px;

    }

  }

  .color1 {

    color: #7499e4;

  }

  .color2 {

    color: #ff7786;

  }

  .color3 {

    color: #339900;

  }

}

</style>

二、chartMin组件代码:

<template>

  <div class="chart-area no-drag" style="position: relative">

    <div id="chartMinline" style="width: 100%; height: 100%" />

    <p

      v-if="tipData"

      :style="{ left: clientX + 'px', top: clientY + 'px' }"

      class="echart-tip"

    >

      <span>时间:{{ tipInfo.date }}</span

      ><br />

      <span>价格:{{ tipInfo.price }}</span

      ><br />

      <span>均价:{{ tipInfo.mittelkurs }}</span

      ><br />

      <span>涨跌幅:{{ tipInfo.change }}%</span><br />

      <span>成交量(手):{{ tipInfo.hand }}</span

      ><br />

      <span>成交额:{{ tipInfo.turnover }}</span>

    </p>

  </div>

</template>

<script setup lang="ts">

import * as echarts from 'echarts'

import _ from 'lodash'

import common from '@/utils/common'

import { toDecimal } from '@/utils/numberFormat'

const props = defineProps({

  id: {

    type: String,

    default: 'chartMin'

  },

  // 折线数据

  dataList: {

    type: Array,

    default: () => []

  },

  // 折线数据

  minDateList1: {

    type: Array,

    default: () => []

  },

  // 小数位数

  digit: {

    type: Number,

    default: () => 2

  },

  // 昨收价

  prePrice: {

    type: Number,

    default: 0

  }

})

var upColor = '#ec0000'

var downColor = '#00da3c'

// 定义图表

const myChart: any = ref(null)

const minDateList = ref<any>(props.minDateList1) // 分时图行情数据

const tipData: any = ref() // 浮框信息

const clientX = ref<any>(0) // 距离左右距离

const clientY = ref<any>(0) // 距离上下距离

const leftMax = ref<any>(0) // 左边Y轴最大值

const leftMin = ref<any>(0) // 左边Y轴最小值

const rightMax = ref<any>(0) // 右边Y轴最大值

const rightMin = ref<any>(0) // 右边Y轴最小值

const leftInterval = ref<any>(0) // 左边分割数

const rightInterval = ref<any>(0) // 右边分割数

const chartData = ref<any>(props.dataList) // 折线数据

const prePrice1 = ref<any>(props.prePrice) // 折线数据

// 图表数据处理

const splitData = (rawData: any) => {

  let categoryData = []

  let allData = []

  let avgValue = []

  let totalVolumeTraded = []

  let totalValueTraded = []

  let changeRatio = []

  for (var i = 0; i < rawData.length; i++) {

    categoryData.push(rawData[i][0])

    allData.push(rawData[i])

    avgValue.push(rawData[i][2])

    totalVolumeTraded.push([i, rawData[i][3], rawData[i][5] > 0 ? 1 : -1])

    totalValueTraded.push(rawData[i][4])

    changeRatio.push(rawData[i][5])

  }

  return {

    categoryData,

    allData,

    avgValue,

    totalVolumeTraded,

    totalValueTraded,

    changeRatio

  }

}

// 使用计算属性创建tipInfo浮框信息

const tipInfo = computed(() => {

  if (!tipData.value) {

    return {

      date: '--',

      price: '0.00',

      change: '0.00',

      mittelkurs: '0.00',

      hand: 0,

      turnover: 0

    }

  }

  const info = {

    date: tipData.value[0],

    price:

      tipData.value[1] == null

        ? '--'

        : tipData.value[1] == 0

        ? '0.00'

        : toDecimal(tipData.value[1], props.digit, true),

    change:

      tipData.value[5] == null

        ? '--'

        : tipData.value[5] == 0

        ? '0.00'

        : tipData.value[5] > 0

        ? `+${toDecimal(tipData.value[5], 2, true)}`

        : toDecimal(tipData.value[5], 2, true),

    mittelkurs:

      tipData.value[2] == null

        ? '--'

        : tipData.value[2] == 0

        ? '0.00'

        : toDecimal(tipData.value[2], props.digit, true),

    hand:

      tipData.value[3] == null

        ? '--'

        : tipData.value[3] == 0

        ? 0

        : common.formatNumUnit(tipData.value[3]),

    turnover:

      tipData.value[4] == null

        ? '--'

        : tipData.value[4] == 0

        ? 0

        : common.formatNumUnit(tipData.value[4])

  }

  return info

})

//监听dataList变化,给图表赋值

watch(

  () => [props.dataList, props.minDateList1, props.prePrice],

  (newValue: any, oldValue: any) => {

    if (newValue[0] != oldValue[0]) {

      // 更新新的图表数据

      chartData.value = newValue[0]

    }

    if (newValue[1] != oldValue[1]) {

      // 更新新的图表数据

      minDateList.value = newValue[1]

    }

    if (newValue[2] != oldValue[2]) {

      // 更新新的图表数据

      prePrice1.value = newValue[2]

    }

    tipData.value = null

    drawLine() // 重新画图

  },

  { deep: true }

)

// 画图

const drawLine = () => {

  // 获取最大值最小值 间隔值

  getMaxMin()

  // 使用getZr添加图表的整个canvas区域的事件

  myChart.value.getZr().on('mouseover', handleMouseEnterMove)

  myChart.value.getZr().on('mousemove', handleMouseEnterMove)

  const chartOption = getChartOption()

  // 绘制图表

  myChart.value.setOption(chartOption)

  window.addEventListener('resize', handleResize, false)

}

// 获取图表option

const getChartOption = () => {

  // 处理datalist数据

  const data = splitData(toRaw(chartData.value))

  const option = {

    color: ['#7499E4', '#FF7786', '#339900'],

    legend: {

      show: true,

      type: 'plain',

      icon: 'roundRect',

      data: ['价格', '均价']

    },

    grid: [

      {

        left: 60,

        right: 70,

        top: '6.4%',

        height: '50%'

      },

      {

        left: 60,

        right: 70,

        top: '68%',

        height: '30%'

      }

    ],

    tooltip: {

      trigger: 'axis',

      // 设置浮框不超出容器

      overflowTooltip: 'none',

      axisPointer: {

        type: 'line',

        lineStyle: {

          type: 'dotted',

          color: '#EDE4FF',

          width: 2

        }

      },

      formatter: function (params: any) {

        const param = params.find((item: any) => item.seriesName == '价格')

        if (param !== undefined && param.data.length > 1) {

          tipData.value = param.data

        } else {

          tipData.value = null

        }

        return ''

      }

    },

    axisPointer: {

      link: { xAxisIndex: 'all' }

    },

    xAxis: [

      {

        type: 'category',

        // 标签

        axisLabel: {

          show: true,

          interval: 29,

          color: '#333',

          showMaxLabel: true

        },

        // 轴线样式

        axisLine: {

          show: false,

          lineStyle: {

            color: '#EDE4FF'

          }

        },

        // 坐标轴刻度

        axisTick: {

          show: true

        },

        data: minDateList.value

      },

      {

        type: 'category',

        gridIndex: 1,

        // 标签

        axisLabel: {

          show: false

        },

        // 轴线样式

        axisLine: {

          show: false,

          lineStyle: {

            color: '#EDE4FF'

          }

        },

        // 坐标轴刻度

        axisTick: {

          show: false

        },

        data: minDateList.value

      }

    ],

    yAxis: [

      {

        type: 'value',

        gridIndex: 0,

        // 坐标轴刻度

        axisTick: {

          show: false

        },

        // 标签

        axisLabel: {

          interval: true,

          color: '#666',

          formatter: function (value: any) {

            return toDecimal(value, props.digit, true)

          }

        },

        // 轴线样式

        axisLine: {

          show: false

        },

        // 坐标轴在 grid 区域中的分隔线

        splitLine: {

          show: false

        },

        min: leftMin.value,

        max: leftMax.value,

        interval: leftInterval.value

      },

      {

        type: 'value',

        gridIndex: 1,

        // 坐标轴刻度

        axisTick: {

          show: false

        },

        // 标签

        axisLabel: {

          interval: true,

          color: '#666',

          formatter: function (value: any) {

            return common.formatNumUnit(value)

          }

        },

        // 轴线样式

        axisLine: {

          show: false

        },

        // 坐标轴在 grid 区域中的分隔线

        splitLine: {

          show: false

        }

      },

      {

        type: 'value',

        gridIndex: 0,

        position: 'right',

        // 坐标轴刻度

        axisTick: {

          show: false

        },

        // 标签

        axisLabel: {

          interval: true,

          color: '#666',

          formatter: function (value: any) {

            return toDecimal(value, 2, true) + '%'

          }

        },

        // 轴线样式

        axisLine: {

          show: false

        },

        // 坐标轴在 grid 区域中的分隔线

        splitLine: {

          show: false

        },

        min: rightMin.value,

        max: rightMax.value,

        interval: rightInterval.value

      }

    ],

    series: [

      {

        name: '价格',

        type: 'line',

        xAxisIndex: 0,

        yAxisIndex: 0,

        showSymbol: false,

        symbolSize: 5,

        smooth: true,

        areaStyle: {

          color: {

            type: 'linear',

            x: 0,

            y: 0,

            x2: 0,

            y2: 1,

            colorStops: [

              {

                offset: 0,

                color: '#D8E0FF' // 0% 处的颜色

              },

              {

                offset: 1,

                color: '#F9FAFF' // 100% 处的颜色

              }

            ],

            global: false // 缺省为 false

          }

        },

        data: data.allData,

        lineStyle: {

          width: 1

        },

        // 标记线

        markLine: {

          silent: true,

          symbol: ['none', 'none'],

          label: {

            show: false

          },

          lineStyle: {

            color: '#7b7de5',

            opacity: 0.5,

            type: 'dot'

          },

          data: [

            {

              name: 'Y 轴值为 yAxis 的水平线',

              yAxis: toDecimal(prePrice1.value, props.digit, true)

            }

          ]

        }

      },

      {

        name: '均价',

        type: 'line',

        xAxisIndex: 0,

        yAxisIndex: 0,

        showSymbol: false,

        smooth: true,

        symbolSize: 5,

        lineStyle: {

          width: 1

        },

        data: data.avgValue

      },

      {

        name: '交易量',

        type: 'bar',

        xAxisIndex: 1,

        yAxisIndex: 1,

        data: data.totalVolumeTraded,

        itemStyle: {

          color: function (params: any) {

            let colorList = ''

            if (params.dataIndex == 0) {

              if (data.allData[0][1] >= prePrice1.value) {

                colorList = upColor

              } else {

                colorList = downColor

              }

            } else {

              if (

                data.allData[params.dataIndex][1] >=

                data.allData[params.dataIndex - 1][1]

              ) {

                colorList = upColor

              } else {

                colorList = downColor

              }

            }

            return colorList

          }

        }

      }

    ]

  }

  return option

}

const getMaxMin = () => {

  if (chartData.value.length > 0) {

    const lstData = chartData.value.filter(

      (m: any) => m[1] != null && m[1] != undefined

    )

    const priceList = lstData.map(function (item: any) {

      return toDecimal(item[1], props.digit, true)

    })

    const averageList = lstData.map(function (item: any) {

      return toDecimal(item[2], props.digit, true)

    })

    const changeRatioList = lstData.map(function (item: any) {

      return toDecimal(item[5], 2, true)

    })

    // 左y轴数据

    var avgMax

    var avgMin

    var priceMax

    var priceMin = 0

    avgMax = getMax(averageList)

    avgMin = getMin(averageList)

    priceMax = getMax(priceList)

    priceMin = getMin(priceList)

    // 股票

    leftMax.value = Math.max(avgMax, priceMax)

    leftMin.value = avgMin == 0 ? priceMin : Math.min(avgMin, priceMin)

    const middleLineVal = prePrice1.value

    const max = common.numSub(leftMax.value, middleLineVal)

    const min = common.numSub(middleLineVal, leftMin.value)

    const absMax = Math.max(Math.abs(Number(max)), Math.abs(Number(min)))

    if (absMax == 0) {

      leftMax.value = common.numMul(middleLineVal, 1.05)

      leftMin.value = common.numMul(middleLineVal, 0.95)

    } else {

      leftMax.value = common.numAdd(middleLineVal, absMax)

      leftMin.value = common.numSub(middleLineVal, absMax)

    }

    leftInterval.value = Number(

      toDecimal(

        common.accDiv(common.numSub(leftMax.value, leftMin.value), 4),

        props.digit + 1,

        true

      )

    )

    // 右y轴数据

    rightMax.value = getMax(changeRatioList)

    rightMin.value = getMin(changeRatioList)

    const middleLineVal1 = 0

    const max1 = rightMax.value - middleLineVal1

    const min1 = middleLineVal1 - rightMin.value

    const absMax1 = Math.max(Math.abs(max1), Math.abs(min1))

    if (absMax1 == 0) {

      rightMax.value = middleLineVal1 * 1.05

      rightMin.value = middleLineVal1 * 0.95

    } else {

      rightMax.value = middleLineVal1 + absMax1

      rightMin.value = middleLineVal1 - absMax1

    }

    rightInterval.value = common.accDiv(

      common.numSub(rightMax.value, rightMin.value),

      4

    )

  }

}

const getMax = (arr: any) => {

  const maxList = arr.filter((item: any) => item !== '-')

  let Max = 0

  if (maxList.length > 0) {

    const max0 = maxList[0]

    Max = max0

    maxList.forEach((item: any) => {

      if (Number(item) > Number(Max)) {

        Max = Number(item)

      }

    })

  }

  return Number(Max)

}

const getMin = (arr: any) => {

  const minList = arr.filter((item: any) => item !== '-')

  let Min = 0

  if (minList.length > 0) {

    const min0 = minList[0]

    Min = min0

    minList.forEach((item: any) => {

      if (Number(item) < Number(Min)) {

        Min = Number(item)

      }

    })

  }

  return Number(Min)

}

const handleResize = () => {

  myChart.value.resize()

}

const handleMouseEnterMove = (params: any) => {

  const { offsetX, offsetY, target, topTarget } = params

  clientX.value = offsetX - 40

  clientY.value = offsetY + 18

  // 移至坐标轴外时target和topTarget都为undefined

  if (!target && !topTarget) {

    tipData.value = null

  }

}

onMounted(() => {

  // 基于准备好的dom,初始化echarts实例

  myChart.value = markRaw(echarts.init(document.getElementById('chartMinline')))

  drawLine()

})

onUnmounted(() => {

  window.removeEventListener('resize', handleResize, false)

})

</script>

<style lang="less" scoped>

.echart-tip {

  position: absolute;

  background-color: rgba(38, 43, 81, 0.5);

  font-size: 12px;

  line-height: 16px;

  padding: 5px;

  border-radius: 4px;

  color: #fff;

  z-index: 9;

  min-width: 130px;

  > p {

    padding: 0;

    margin: 0;

  }

}

</style>

三、chartK组件代码:

<template>

  <div

    class="chart-area no-drag"

    style="position: relative"

    v-loading="loading"

  >

    <div id="chartKline" style="width: 100%; height: 100%" />

    <p

      v-if="tipData"

      :style="{ left: clientX + 'px', top: clientY + 'px' }"

      class="echart-tip"

    >

      <span>{{ tipInfo.axisValue }}</span

      ><br />

      <span>开盘:{{ tipInfo.opening }}</span

      ><br />

      <span>收盘:{{ tipInfo.closing }}</span

      ><br />

      <span>最低:{{ tipInfo.bottommost }}</span

      ><br />

      <span>最高:{{ tipInfo.highest }}</span

      ><br />

      <span>涨跌幅:{{ tipInfo.change }}%</span><br />

      <span>成交量(手):{{ tipInfo.turnover }}</span

      ><br />

      <span>MA5:{{ tipInfo.MA5 }}</span

      ><br />

      <span>MA10:{{ tipInfo.MA10 }}</span

      ><br />

      <span>MA20:{{ tipInfo.MA20 }}</span

      ><br />

      <span>MA30:{{ tipInfo.MA30 }}</span>

    </p>

  </div>

</template>

<script setup lang="ts">

import * as echarts from 'echarts'

import common from '@/utils/common'

import { toDecimal } from '@/utils/numberFormat'

const props = defineProps({

  // 指标 1:成交量 2.MACD 3.KDJ

  currentIndex: {

    type: Number,

    default: 1

  },

  // 折线数据 时间 开盘价 收盘价 最低值 最高值 总量

  dataList: {

    type: Array,

    default: () => []

  },

  // 小数位数

  digit: {

    type: Number,

    default: () => 2

  },

  // 当前选择的K线周期 1:日K 2:周K 3:月K 4:5min 5:30min 6:60min

  currentTab: {

    type: Number,

    default: () => 1

  }

})

const emit = defineEmits(['getHoverData'])

const upColor = '#ec0000'

const downColor = '#00da3c'

const ma5Color = '#39afe6'

const ma10Color = '#da6ee8'

const ma20Color = '#ffab42'

const ma30Color = '#00940b'

const color1 = '#7499E4'

const color2 = '#FF7786'

const color3 = '#339900'

const dataListTemp = ref<any>(props.dataList) // 备份dataList

const isDrawing = ref(false) // 是否展示图表

const loading = ref(false) // 是否展示图表

const clientX = ref<any>(0) // 距离左右距离

const clientY = ref<any>(0) // 距离上下距离

// 定义图表

const myChart: any = ref(null)

const tipData: any = ref(null) // 浮框信息

const dataZoomY: any = ref(null) // 保存dataZoomY信息

// 图表数据处理

const splitData = (rawData: any) => {

  const categoryData = []

  const values = []

  const volumes = []

  const MACD = []

  const DEA = []

  const DIF = []

  const D = []

  const J = []

  const K = []

  const RSI6 = []

  const RSI12 = []

  const RSI24 = []

  for (let i = 0; i < rawData.length; i++) {

    categoryData.push(rawData[i][0])

    values.push(rawData[i].slice(1))

    volumes.push([i, rawData[i][5], rawData[i][1] > rawData[i][2] ? 1 : -1])

    MACD.push([i, rawData[i][8], rawData[i][8] < 0 ? 1 : -1])

    DEA.push(rawData[i][9])

    DIF.push(rawData[i][10])

    D.push(rawData[i][11])

    J.push(rawData[i][12])

    K.push(rawData[i][13])

    RSI6.push(rawData[i][14])

    RSI12.push(rawData[i][15])

    RSI24.push(rawData[i][16])

  }

  if (rawData.length <= 70) {

    for (let index = 0; index < 70 - rawData.length; index++) {

      categoryData.push('')

      values.push([])

      volumes.push(['', '', ''])

      MACD.push(['', '', ''])

      DEA.push(0)

      DIF.push(0)

      D.push(0)

      J.push(0)

      K.push(0)

      RSI6.push(0)

      RSI12.push(0)

      RSI24.push(0)

    }

  }

  return {

    categoryData,

    values,

    volumes,

    MACD,

    DEA,

    DIF,

    D,

    J,

    K,

    RSI6,

    RSI12,

    RSI24

  }

}

// 使用计算属性创建tipInfo浮框信息

const tipInfo = computed(() => {

  if (!tipData.value) {

    return {

      axisValue: '--',

      opening: '0.00',

      closing: '0.00',

      bottommost: '0.00',

      highest: '0.00',

      change: '0.00',

      turnover: 0,

      MA5: '--',

      MA10: '--',

      MA20: '--',

      MA30: '--'

    }

  }

  const data = tipData.value.data

  const info = {

    axisValue: tipData.value.axisValue,

    opening:

      data[1] == null

        ? '--'

        : data[1] == 0

        ? '0.00'

        : toDecimal(data[1], props.digit, true),

    closing:

      data[2] == null

        ? '--'

        : data[2] == 0

        ? '0.00'

        : toDecimal(data[2], props.digit, true),

    bottommost:

      data[3] == null

        ? '--'

        : data[3] == 0

        ? '0.00'

        : toDecimal(data[3], props.digit, true),

    highest:

      data[4] == null

        ? '--'

        : data[4] == 0

        ? '0.00'

        : toDecimal(data[4], props.digit, true),

    change:

      data[7] == null

        ? '--'

        : data[7] == 0

        ? '0.00'

        : data[7] > 0

        ? `+${toDecimal(data[7], props.digit, true)}`

        : toDecimal(data[7], props.digit, true),

    turnover:

      data[5] == null ? '--' : data[5] == 0 ? 0 : common.formatNumUnit(data[5]),

    MA5: isNaN(tipData.value.MA5) ? '--' : tipData.value.MA5,

    MA10: isNaN(tipData.value.MA10) ? '--' : tipData.value.MA10,

    MA20: isNaN(tipData.value.MA20) ? '--' : tipData.value.MA20,

    MA30: isNaN(tipData.value.MA30) ? '--' : tipData.value.MA30

  }

  return info

})

//监听currentIndex与dataList变化,给图表赋值

watch(

  () => [props.currentIndex, props.dataList, props.currentTab],

  (newValue: any, oldValue: any) => {

    if (newValue[0] != oldValue[0]) {

      initHoverData()

      drawLine()

    }

    if (newValue[1] != oldValue[1]) {

      dataListTemp.value = newValue[1]

      myChart.value && myChart.value.showLoading()

      initHoverData()

      drawLine()

    }

    if (newValue[2] != oldValue[2]) {

      resetChartDrawing()

      initHoverData()

    }

  },

  { deep: true }

)

const init = () => {

  // 基于准备好的dom,初始化echarts实例

  myChart.value = markRaw(echarts.init(document.getElementById('chartKline')))

  myChart.value.getZr().on('click', handleEchartsClick)

  // 使用getZr添加图表的整个canvas区域的事件

  myChart.value.getZr().on('mouseover', handleMouseEnterMove)

  myChart.value.getZr().on('mousemove', handleMouseEnterMove)

  myChart.value.on('dataZoom', (event: any) => {

    if (event.batch) {

      event = event.batch[0]

      dataZoomY.value = event

    } else {

      const { dataZoomId } = event

      if (!dataZoomId) {

        return

      }

      dataZoomY.value = event

    }

  })

  initHoverData()

  drawLine()

  window.addEventListener('resize', handleResize, false)

}

const calculateMA = (dayCount: any, data: any) => {

  const result = []

  for (let i = 0, len = data.categoryData.length; i < len; i++) {

    if (i < dayCount - 1) {

      result.push('-')

      continue

    }

    let sum = 0

    for (let j = 0; j < dayCount; j++) {

      sum += Number(data.values[i - j][1])

    }

    result.push((sum / dayCount).toFixed(props.digit))

  }

  return result

}

const drawLine = () => {

  // 基于准备好的dom,初始化echarts实例

  if (isDrawing.value || !myChart.value) {

    setTimeout(() => {

      drawLine()

    })

    return

  }

  isDrawing.value = true

  const chartOption = getChartOption()

  // 绘制图表

  isDrawing.value && myChart.value.setOption(chartOption, true)

  nextTick(() => {

    isDrawing.value = false

    myChart.value.hideLoading()

  })

}

// 获取图表option

const getChartOption = () => {

  loading.value = true

  // 处理datalist数据

  const data = splitData(dataListTemp.value)

  let dataZoomStart = getStart()

  let dataZoomEnd = 100

  if (isDrawing.value && dataZoomY.value) {

    const { start, end } = dataZoomY.value

    dataZoomStart = start

    dataZoomEnd = end

  }

  const option: any = {

    animation: false,

    legend: {

      // 图例控件,点击图例控制哪些系列不显示

      icon: 'rect',

      type: 'scroll',

      itemWidth: 14,

      itemHeight: 2,

      right: 30,

      top: -6,

      animation: true,

      fontSize: 12,

      color: '#999999',

      pageIconColor: '#999999',

      selectedMode: false,

      data: ['MA5', 'MA10', 'MA20', 'MA30']

    },

    color: [ma5Color, ma5Color, ma10Color, ma20Color, ma30Color],

    grid: [

      {

        left: 60,

        right: 30,

        top: '5.25%',

        height: '40%'

      },

      {

        left: 60,

        right: 30,

        top: '58%',

        height: '25%'

      }

    ],

    axisPointer: {

      link: { xAxisIndex: 'all' }, // 绑定两个图

      label: {

        backgroundColor: '#777'

      }

    },

    tooltip: {

      trigger: 'axis',

      axisPointer: {

        type: 'cross',

        lineStyle: {

          color: '#999',

          width: 2

        }

      },

      extraCssText: 'text-align: left;',

      formatter: function (params: any) {

        setHoverData(params)

        const param = params.find(

          (item: any) =>

            item.axisIndex === 0 && item.componentSubType === 'candlestick'

        )

        if (param && param.data && param.data.length > 1) {

          const MA5Item = params.find((item: any) => item.seriesName == 'MA5')

          const MA5 = MA5Item ? toDecimal(MA5Item.data, props.digit, true) : 0

          const MA10Item = params.find(

            (item: any) => item.seriesName === 'MA10'

          )

          const MA10 = MA10Item

            ? toDecimal(MA10Item.data, props.digit, true)

            : 0

          const MA20Item = params.find(

            (item: any) => item.seriesName === 'MA20'

          )

          const MA20 = MA20Item

            ? toDecimal(MA20Item.data, props.digit, true)

            : 0

          const MA30Item = params.find(

            (item: any) => item.seriesName === 'MA30'

          )

          const MA30 = MA30Item

            ? toDecimal(MA30Item.data, props.digit, true)

            : 0

          tipData.value = Object.assign({}, param, {

            MA5,

            MA10,

            MA20,

            MA30

          })

        } else {

          tipData.value = null

        }

        return ''

      }

    },

    xAxis: [

      {

        type: 'category',

        // 标签

        axisLabel: {

          show: true,

          color: '#333'

        },

        // 轴线样式

        axisLine: {

          show: false,

          lineStyle: {

            color: '#333'

          }

        },

        // 坐标轴刻度

        axisTick: {

          show: false

        },

        data: data.categoryData

      },

      {

        type: 'category',

        gridIndex: 1,

        // 标签

        axisLabel: {

          show: false

        },

        // 轴线样式

        axisLine: {

          show: false,

          lineStyle: {

            color: '#333'

          }

        },

        // 坐标轴刻度

        axisTick: {

          show: false

        },

        // 坐标轴指示器

        axisPointer: {

          label: {

            show: false

          }

        },

        data: data.categoryData

      }

    ],

    yAxis: [

      {

        type: 'value',

        gridIndex: 0,

        scale: true,

        splitNumber: 5,

        // 坐标轴刻度

        axisTick: {

          show: false

        },

        // 标签

        axisLabel: {

          interval: true,

          color: '#666',

          formatter: function (value: any) {

            return toDecimal(value, props.digit, true)

          }

        },

        // 轴线样式

        axisLine: {

          show: false

        }

      },

      // 交易量轴

      {

        type: 'value',

        gridIndex: 1,

        // y轴原点是否不从0开始

        scale: true,

        // 坐标轴刻度

        axisTick: {

          show: false

        },

        // 标签

        axisLabel: {

          interval: true,

          color: '#666',

          formatter: function (value: any) {

            return common.formatNumUnit(value)

          }

        },

        // 轴线样式

        axisLine: {

          show: false

        },

        // 坐标轴在 grid 区域中的分隔线

        splitLine: {

          show: false

        }

      }

    ],

    series: [

      {

        name: 'k线',

        type: 'candlestick',

        itemStyle: {

          color: upColor,

          color0: downColor,

          borderColor: upColor,

          borderColor0: downColor

        },

        xAxisIndex: 0,

        yAxisIndex: 0,

        data: data.values,

        lineStyle: {

          width: 1

        }

      },

      {

        name: '交易量',

        type: 'bar',

        xAxisIndex: 1,

        yAxisIndex: 1,

        data: data.volumes,

        itemStyle: {

          color: function (params: any) {

            let colorList = ''

            if (params.dataIndex == 0) {

              if (data.values[0][1] >= data.values[0][0]) {

                colorList = upColor

              } else {

                colorList = downColor

              }

            } else {

              if (

                data.values[params.dataIndex][1] >=

                data.values[params.dataIndex - 1][1]

              ) {

                colorList = upColor

              } else {

                colorList = downColor

              }

            }

            return colorList

          }

        }

      },

      {

        name: 'MA5',

        type: 'line',

        data: calculateMA(5, data),

        smooth: true,

        symbol: 'none', // 隐藏选中时有小圆点

        lineStyle: {

          opacity: 0.8,

          color: ma5Color,

          width: 1

        }

      },

      {

        name: 'MA10',

        type: 'line',

        data: calculateMA(10, data),

        smooth: true,

        symbol: 'none',

        lineStyle: {

          // 标线的样式

          opacity: 0.8,

          color: ma10Color,

          width: 1

        }

      },

      {

        name: 'MA20',

        type: 'line',

        data: calculateMA(20, data),

        smooth: true,

        symbol: 'none',

        lineStyle: {

          opacity: 0.8,

          width: 1,

          color: ma20Color

        }

      },

      {

        name: 'MA30',

        type: 'line',

        data: calculateMA(30, data),

        smooth: true,

        symbol: 'none',

        lineStyle: {

          opacity: 0.8,

          width: 1,

          color: ma30Color

        }

      }

    ],

    dataZoom: [

      {

        id: 'dataZoomX',

        type: 'inside',

        xAxisIndex: [0, 1],

        start: dataZoomStart,

        end: dataZoomEnd

      },

      {

        id: 'dataZoomY',

        show: true,

        xAxisIndex: [0, 1],

        type: 'slider',

        height: 20, // 设置滑动条的高度

        realtime: true,

        bottom: 7,

        start: dataZoomStart,

        end: dataZoomEnd

      }

    ]

  }

  if (props.currentIndex == 2) {

    option.series[1] = {

      name: 'MACD',

      type: 'bar',

      xAxisIndex: 1,

      yAxisIndex: 1,

      data: data.MACD,

      showSymbol: false

    }

    option.visualMap = {

      show: false,

      seriesIndex: 1,

      dimension: 2,

      pieces: [

        {

          value: 1,

          color: downColor

        },

        {

          value: -1,

          color: upColor

        }

      ]

    }

    option.series.push({

      name: 'DEA',

      type: 'line',

      xAxisIndex: 1,

      yAxisIndex: 1,

      data: data.DEA,

      showSymbol: false,

      lineStyle: {

        color: color1

      }

    })

    option.series.push({

      name: 'DIF',

      type: 'line',

      xAxisIndex: 1,

      yAxisIndex: 1,

      data: data.DIF,

      showSymbol: false,

      lineStyle: {

        color: color2

      }

    })

  } else if (props.currentIndex == 3) {

    option.series.push({

      name: 'K',

      type: 'line',

      xAxisIndex: 1,

      yAxisIndex: 1,

      data: data.K,

      showSymbol: false,

      lineStyle: {

        color: color1

      }

    })

    option.series[1] = {

      name: 'D',

      type: 'line',

      xAxisIndex: 1,

      yAxisIndex: 1,

      data: data.D,

      showSymbol: false,

      lineStyle: {

        color: color2

      }

    }

    option.series.push({

      name: 'J',

      type: 'line',

      xAxisIndex: 1,

      yAxisIndex: 1,

      data: data.J,

      showSymbol: false,

      lineStyle: {

        color: color3

      }

    })

  } else if (props.currentIndex == 4) {

    option.series[1] = {

      name: 'RSI6',

      type: 'line',

      xAxisIndex: 1,

      yAxisIndex: 1,

      data: data.RSI6,

      showSymbol: false,

      lineStyle: {

        color: color1

      }

    }

    option.series.push({

      name: 'RSI12',

      type: 'line',

      xAxisIndex: 1,

      yAxisIndex: 1,

      data: data.RSI12,

      showSymbol: false,

      lineStyle: {

        color: color2

      }

    })

    option.series.push({

      name: 'RSI24',

      type: 'line',

      xAxisIndex: 1,

      yAxisIndex: 1,

      data: data.RSI24,

      showSymbol: false,

      lineStyle: {

        color: color3

      }

    })

  }

  loading.value = false

  return option

}

const setHoverData = (params: any) => {

  const param = params.find(function (item: any) {

    return item.componentSubType == 'candlestick'

  })

  if (param !== undefined) {

    emit('getHoverData', param.data)

  }

}

const initHoverData = () => {

  const data: any = dataListTemp.value

  if (data.length > 0) {

    let arr = [

      '',

      '',

      '',

      '',

      '',

      data[data.length - 1][5],

      '',

      '',

      data[data.length - 1][8],

      data[data.length - 1][9],

      data[data.length - 1][10],

      data[data.length - 1][11],

      data[data.length - 1][12],

      data[data.length - 1][13],

      data[data.length - 1][14],

      data[data.length - 1][15],

      data[data.length - 1][16]

    ]

    emit('getHoverData', arr)

  }

}

// 获取起始位置

const getStart = () => {

  if (dataListTemp.value && dataListTemp.value.length > 0) {

    const start =

      dataListTemp.value.length > 70

        ? 100 - (70 / dataListTemp.value.length) * 100

        : 0

    loading.value = false

    return start

  } else {

    let start = 0

    switch (props.currentTab) {

      case 1:

        start = 95

        break

      case 2:

        start = 95

        break

      case 3:

        start = 95

        break

      case 4:

        start = 95

        break

      case 5:

        start = 95

        break

      case 6:

        start = 95

        break

      default:

        start = 95

    }

    loading.value = false

    return start

  }

}

const resetChartDrawing = () => {

  dataZoomY.value = null

  isDrawing.value = false

  tipData.value = null

}

const handleResize = () => {

  myChart.value.resize()

}

const handleMouseEnterMove = (params: any) => {

  const { offsetX, offsetY, target, topTarget } = params

  clientX.value = offsetX - 40

  clientY.value = offsetY + 18

  // 移至坐标轴外时target和topTarget都为undefined

  if (!target && !topTarget) {

    tipData.value = null

    initHoverData()

  }

}

// 点击事件

const handleEchartsClick = (params: any) => {

  const pointInPixel = [params.offsetX, params.offsetY]

  if (myChart.value.containPixel('grid', pointInPixel)) {

    const pointInGrid = myChart.value.convertFromPixel(

      {

        seriesIndex: 0

      },

      pointInPixel

    )

    const xIndex = pointInGrid[0] // 索引

    const handleIndex = Number(xIndex)

    const seriesObj = myChart.value.getOption() // 图表object对象

  }

}

onMounted(() => {

  nextTick(() => {

    init()

  })

})

onUnmounted(() => {

  window.removeEventListener('resize', handleResize, false)

})

</script>

<style lang="less" scoped>

.echart-tip {

  position: absolute;

  background-color: rgba(38, 43, 81, 0.5);

  font-size: 12px;

  line-height: 16px;

  padding: 5px;

  border-radius: 4px;

  color: #fff;

  z-index: 9;

  min-width: 130px;

  > p {

    padding: 0;

    margin: 0;

  }

}

</style>

四、useWebSocket.ts文件代码:

const DEFAULT_OPTIONS = {

  url: '', // websocket url

  heartBeatData: '', // 你的心跳数据

  heartBeatInterval: 60 * 1000, // 心跳间隔,单位ms

  reconnectInterval: 5000, // 断线重连间隔,单位ms

  maxReconnectAttempts: 10 // 最大重连次数

}

export const SocketStatus = {

  Connecting: '正在连接...', //表示正在连接,这是初始状态。

  Connected: '连接已建立', //表示连接已经建立。

  Disconnecting: '连接正在关闭', //表示连接正在关闭。

  Disconnected: '连接已断开' //表示连接已经关闭

}

const SocketCloseCode = 1000

export default function useWebSocket(options = {}) {

  const state = {

    options: { ...DEFAULT_OPTIONS, ...options },

    socket: null,

    reconnectAttempts: 0,

    reconnectTimeout: null,

    heartBetaSendTimer: null, // 心跳发送定时器

    heartBetaTimeoutTimer: null // 心跳超时定时器

  }

  // 连接状态

  const status = ref(SocketStatus.Disconnected)

  const message = ref(null)

  const error = ref(null)

  // 连接

  const connect = () => {

    disconnect()

    status.value = SocketStatus.Connecting

    if (!window.navigator.onLine) {

      setTimeout(() => {

        status.value = SocketStatus.Disconnected

      }, 500)

      return

    }

    //@ts-ignore

    state.socket = new WebSocket(state.options.url) as WebSocket

    //@ts-ignore

    state.socket.onopen = (openEvent:any) => {

      // console.log('socket连接:', openEvent)

      state.reconnectAttempts = 0

      status.value = SocketStatus.Connected

      error.value = null

      startHeartBeat()

    }

    //@ts-ignore

    state.socket.onmessage = (msgEvent: any) => {

      // console.log('socket消息:', msgEvent)

      // 收到任何数据,重新开始心跳

      startHeartBeat()

      const { data } = msgEvent

      const msg = JSON.parse(data)

      //心跳数据, 可自行修改

      if (+msg.msg_id === 0) {

        return

      }

      message.value = msg

    }

    //@ts-ignore

    state.socket.onclose = (closeEvent: any) => {

      // console.log('socket关闭:', closeEvent)

      status.value = SocketStatus.Disconnected

      // 非正常关闭,尝试重连

      if (closeEvent.code !== SocketCloseCode) {

        reconnect()

      }

    }

    //@ts-ignore

    state.socket.onerror = (errEvent: any) => {

      // console.log('socket报错:', errEvent)

      status.value = SocketStatus.Disconnected

      error.value = errEvent

      // 连接失败,尝试重连

      reconnect()

    }

  }

  const disconnect = () => {

    //@ts-ignore

    if (state.socket && (state.socket.OPEN || state.socket.CONNECTING)) {

      // console.log('socket断开连接')

      status.value = SocketStatus.Disconnecting

      //@ts-ignore

      state.socket.onmessage = null

      //@ts-ignore

      state.socket.onerror = null

      //@ts-ignore

      state.socket.onclose = null

      // 发送关闭帧给服务端

      //@ts-ignore

      state.socket.close(SocketCloseCode, 'normal closure')

      status.value = SocketStatus.Disconnected

      state.socket = null

    }

    stopHeartBeat()

    stopReconnect()

  }

  const startHeartBeat = () => {

    stopHeartBeat()

    onHeartBeat(() => {

      if (status.value === SocketStatus.Connected) {

        //@ts-ignore

        state.socket.send(state.options.heartBeatData)

        // console.log('socket心跳发送:', state.options.heartBeatData)

      }

    })

  }

  const onHeartBeat = (callback: any) => {

    //@ts-ignore

    state.heartBetaSendTimer = setTimeout(() => {

      callback && callback()

      //@ts-ignore

      state.heartBetaTimeoutTimer = setTimeout(() => {

        // 心跳超时,直接关闭socket,抛出自定义code=4444, onclose里进行重连

        //@ts-ignore

        state.socket.close(4444, 'heart timeout')

      }, state.options.heartBeatInterval)

    }, state.options.heartBeatInterval)

  }

  const stopHeartBeat = () => {

    state.heartBetaSendTimer && clearTimeout(state.heartBetaSendTimer)

    state.heartBetaTimeoutTimer && clearTimeout(state.heartBetaTimeoutTimer)

  }

  // 重连

  const reconnect = () => {

    if (status.value === SocketStatus.Connected || status.value === SocketStatus.Connecting) {

      return

    }

    stopHeartBeat()

    if (state.reconnectAttempts < state.options.maxReconnectAttempts) {

      // console.log('socket重连:', state.reconnectAttempts)

      // 重连间隔,5秒起步,下次递增1秒

      const interval = Math.max(state.options.reconnectInterval, state.reconnectAttempts * 1000)

      // console.log('间隔时间:', interval)

      //@ts-ignore

      state.reconnectTimeout = setTimeout(() => {

        if (status.value !== SocketStatus.Connected && status.value !== SocketStatus.Connecting) {

          connect()

        }

      }, interval)

      state.reconnectAttempts += 1

    } else {

      status.value = SocketStatus.Disconnected

      stopReconnect()

    }

  }

  // 停止重连

  const stopReconnect = () => {

    state.reconnectTimeout && clearTimeout(state.reconnectTimeout)

  }

  return {

    status,

    message,

    error,

    connect,

    disconnect

  }

}

五、common.ts文件代码:

// import XLSX from 'xlsx';

import CST from './constant'

import { toDecimal } from './numberFormat'

const common = {

  addDate(date: any, days: any) {

    if (days == undefined || days == '') {

      days = 1

    }

    // var date = new Date(date)

    date.setDate(date.getDate() + days)

    const month = date.getMonth() + 1

    const day = date.getDate()

    return (

      date.getFullYear() +

      '/' +

      this.getFormatDate(month) +

      '/' +

      this.getFormatDate(day)

    )

  },

  // 小数相减精确算法

  numSub(data1: any, data2: any) {

    let num = 0

    let num1 = 0

    let num2 = 0

    let precision = 0 // 精度

    try {

      num1 = data1.toString().split('.')[1].length

    } catch (e) {

      num1 = 0

    }

    try {

      num2 = data2.toString().split('.')[1].length

    } catch (e) {

      num2 = 0

    }

    num = Math.pow(10, Math.max(num1, num2))

    precision = num1 >= num2 ? num1 : num2

    return ((data1 * num - data2 * num) / num).toFixed(precision)

  },

  // 日期月份/天的显示,如果是1位数,则在前面加上'0'

  getFormatDate(arg: any) {

    if (arg == undefined || arg == '') {

      return ''

    }

    let re = arg + ''

    if (re.length < 2) {

      re = '0' + re

    }

    return re

  },

  isArray: function (obj: any) {

    return Object.prototype.toString.call(obj) === '[object Array]'

  },

  isEmpty(obj: any) {

    obj = obj + ''

    if (

      typeof obj === 'undefined' ||

      obj == null ||

      obj.replace(/(^\s*)|(\s*$)/g, '') === ''

    ) {

      return true

    } else {

      return false

    }

  },

  // 小数相加精确算法

  numAdd(arg1: any, arg2: any) {

    let r1 = 0

    let r2 = 0

    let r3 = 0

    try {

      r1 = (arg1 + '').split('.')[1].length

    } catch (err) {

      r1 = 0

    }

    try {

      r2 = (arg2 + '').split('.')[1].length

    } catch (err) {

      r2 = 0

    }

    r3 = Math.pow(10, Math.max(r1, r2))

    return (this.numMul(arg1, r3) + this.numMul(arg2, r3)) / r3

  },

  // 判断小数位数

  getDecLen(value: number) {

    if (!value) {

      return 0

    }

    const strVal = value.toString()

    if (!strVal.includes('.')) {

      return 0

    }

    return strVal.split('.')[1].length

  },

  // 两数相除

  accDiv(num1: any, num2: any) {

    let t1, t2

    try {

      t1 = num1.toString().split('.')[1].length

    } catch (e) {

      t1 = 0

    }

    try {

      t2 = num2.toString().split('.')[1].length

    } catch (e) {

      t2 = 0

    }

    const r1 = Number(num1.toString().replace('.', ''))

    const r2 = Number(num2.toString().replace('.', ''))

    return (r1 / r2) * Math.pow(10, t2 - t1)

  },

  formatDate: function (date: any, format: any) {

    let v = ''

    if (typeof date === 'string' || typeof date !== 'object') {

      return

    }

    const year = date.getFullYear()

    const month = date.getMonth() + 1

    const day = date.getDate()

    const hour = date.getHours()

    const minute = date.getMinutes()

    const second = date.getSeconds()

    const weekDay = date.getDay()

    const ms = date.getMilliseconds()

    let weekDayString = ''

    if (weekDay === 1) {

      weekDayString = '星期一'

    } else if (weekDay === 2) {

      weekDayString = '星期二'

    } else if (weekDay === 3) {

      weekDayString = '星期三'

    } else if (weekDay === 4) {

      weekDayString = '星期四'

    } else if (weekDay === 5) {

      weekDayString = '星期五'

    } else if (weekDay === 6) {

      weekDayString = '星期六'

    } else if (weekDay === 0) {

      weekDayString = '星期日'

    }

    v = format

    // Year

    v = v.replace(/yyyy/g, year)

    v = v.replace(/YYYY/g, year)

    v = v.replace(/yy/g, (year + '').substring(2, 4))

    v = v.replace(/YY/g, (year + '').substring(2, 4))

    // Month

    const monthStr = '0' + month

    v = v.replace(/MM/g, monthStr.substring(monthStr.length - 2))

    // Day

    const dayStr = '0' + day

    v = v.replace(/dd/g, dayStr.substring(dayStr.length - 2))

    // hour

    const hourStr = '0' + hour

    v = v.replace(/HH/g, hourStr.substring(hourStr.length - 2))

    v = v.replace(/hh/g, hourStr.substring(hourStr.length - 2))

    // minute

    const minuteStr = '0' + minute

    v = v.replace(/mm/g, minuteStr.substring(minuteStr.length - 2))

    // Millisecond

    v = v.replace(/sss/g, ms)

    v = v.replace(/SSS/g, ms)

    // second

    const secondStr = '0' + second

    v = v.replace(/ss/g, secondStr.substring(secondStr.length - 2))

    v = v.replace(/SS/g, secondStr.substring(secondStr.length - 2))

    // weekDay

    v = v.replace(/E/g, weekDayString)

    return v

  },

  /**

   * 判断是否同周,输入时间date1小于date2

   * @param {*} date1

   * @param {*} date2

   */

  isSameWeek: function (date1: any, date2: any) {

    const day1 = new Date(date1).getDay() == 0 ? 7 : new Date(date1).getDay()

    const day2 = new Date(date2).getDay() == 0 ? 7 : new Date(date2).getDay()

    const time1 = new Date(date1).getTime()

    const time2 = new Date(date2).getTime()

    if (day1 >= day2) {

      return false

    } else {

      return time2 - time1 < 7 * 24 * 3600 * 1000

    }

  },

  getUrlKey: function (name: any) {

    // eslint-disable-next-line no-sparse-arrays

    return (

      decodeURIComponent(

        //@ts-ignore

        (new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(

          location.href

          // eslint-disable-next-line no-sparse-arrays

        ) || [, ''])[1].replace(/\+/g, '%20')

      ) || null

    )

  },

  getUrlParam: function (name: any) {

    const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')

    const r = window.location.search.substr(1).match(reg)

    if (r != null) return unescape(r[2])

    return null

  },

  setPorpsReadonly(props: any) {

    for (const col in props) {

      if (props[col].Columns && common.isArray(props[col].Columns)) {

        props[col].require = 'false'

        props[col].isImport = 'false'

        props[col].ReadOnly = 'true'

        props[col].Columns.forEach((e: any) => {

          e.readonly = 'true'

        })

      } else {

        for (const co in props[col]) {

          props[col][co].readonly = 'true'

        }

      }

    }

    return props

  },

  // 根据表单里的 oldinstanceid 判断是否是非首次报备的单

  isFirstFormByOldInstanceId(value: any, instanceId: any) {

    let isfirst = true

    instanceId = instanceId + ''

    for (const col in value) {

      if (!common.isArray(value[col])) {

        if (value[col].oldflowinstanceid) {

          if (

            value[col].oldflowinstanceid !== '' &&

            value[col].oldflowinstanceid !== instanceId

          ) {

            isfirst = false

            break

          }

        }

      }

    }

    return isfirst

  },

  setPropNotFrist(props: any) {

    for (const col in props) {

      // eslint-disable-next-line no-empty

      if (props[col].Columns && common.isArray(props[col].Columns)) {

      } else {

        for (const co in props[col]) {

          if (props[col][co].objectupdate !== 'true') {

            props[col][co].readonly = 'true'

          }

        }

      }

    }

    return props

  },

  /**

   * 精确乘

   * @param arg1

   * @param arg2

   * @returns {number}

   */

  numMul(arg1: any, arg2: any) {

    const r1 = arg1 + ''

    const r2 = arg2 + ''

    let r3 = 0

    let r4 = 0

    try {

      r3 = r1.split('.')[1].length

    } catch (err) {

      r3 = 0

    }

    try {

      r4 = r2.split('.')[1].length

    } catch (err) {

      r4 = 0

    }

    return (

      (Number(r1.replace('.', '')) * Number(r2.replace('.', ''))) /

      Math.pow(10, r4 + r3)

    )

  },

  /**

   * 精确除

   * @param arg1

   * @param arg2

   * @returns {number}

   */

  numDiv(arg1: any, arg2: any) {

    const r1 = arg1 + ''

    const r2 = arg2 + ''

    let r3 = 0

    let r4 = 0

    try {

      r3 = r1.split('.')[1].length

    } catch (err) {

      r3 = 0

    }

    try {

      r4 = r2.split('.')[1].length

    } catch (err) {

      r4 = 0

    }

    return this.numMul(

      Number(r1.replace('.', '')) / Number(r2.replace('.', '')),

      Math.pow(10, r4 - r3)

    )

  },

  /**

   * 精确取余

   * @param arg1

   * @param arg2

   * @returns {number}

   */

  numRem(arg1: any, arg2: any) {

    let r1 = 0

    let r2 = 0

    let r3 = 0

    try {

      r1 = (arg1 + '').split('.')[1].length

    } catch (err) {

      r1 = 0

    }

    try {

      r2 = (arg2 + '').split('.')[1].length

    } catch (err) {

      r2 = 0

    }

    r3 = Math.pow(10, Math.max(r1, r2))

    return (this.numMul(arg1, r3) % this.numMul(arg2, r3)) / r3

  },

  formatNumUnit(value_: any) {

    const value = Math.abs(value_) // 1

    const newValue = ['', '', '']

    let fr = 1000

    let num = 3

    let fm = 1

    while (value / fr >= 1) {

      fr *= 10

      num += 1

    }

    if (num <= 4) {

      // 千

      newValue[0] = value + ''

    } else if (num <= 8) {

      // 万

      fm = 10000

      if (value % fm === 0) {

        //@ts-ignore

        newValue[0] = parseInt(value / fm) + ''

      } else {

        //@ts-ignore

        newValue[0] = parseFloat(value / fm).toFixed(2) + ''

      }

      // newValue[1] = text1

      newValue[1] = '万'

    } else if (num <= 16) {

      // 亿

      fm = 100000000

      if (value % fm === 0) {

        //@ts-ignore

        newValue[0] = parseInt(value / fm) + ''

      } else {

        //@ts-ignore

        newValue[0] = parseFloat(value / fm).toFixed(2) + ''

      }

      newValue[1] = '亿'

    }

    if (value < 1000) {

      newValue[0] = value + ''

      newValue[1] = ''

    }

    let text = newValue.join('')

    if (value_ < 0) {

      text = '-' + text

    }

    return text

  },

  // 获取行情小数位数(最新价、涨跌、买价、卖价)

  getTickDecLen(securityType: any, market: any, plateID: any) {

    // 沪深A股 -> 2

    if (securityType == CST.SecurityType.Stock) {

      return 2

    }

    // 基金 -> 3

    if (securityType == CST.SecurityType.Fund) {

      return 3

    }

    // 债券 -> 上海市场除国债逆回购,小数点后保留2位小数,国债逆回购3位小数;深圳市场保留3位小数

    if (securityType == CST.SecurityType.Bond) {

      // 深圳市场

      if (market == CST.Market.SZSE) {

        return 3

      }

      // 上海市场

      if (market == CST.Market.SSE) {

        // 国债逆回购

        if (plateID == CST.PlateID.ZQHG_Bond) {

          return 3

        }

        return 3

      }

    }

    return 2

  },

  // 转换成交量单位

  cvtVolumeUnit(volume: any, market: any, securityType: any) {

    // 深圳市场

    if (market == CST.Market.SZSE) {

      // 股票、基金、指数

      if (

        securityType == CST.SecurityType.Stock ||

        securityType == CST.SecurityType.Fund ||

        securityType == CST.SecurityType.Index

      ) {

        return volume / 100

      }

      // 债券

      if (securityType == CST.SecurityType.Bond) {

        return volume / 10

      }

    }

    // 上海市场

    if (market == CST.Market.SSE) {

      // 股票、基金、指数

      if (

        securityType == CST.SecurityType.Stock ||

        securityType == CST.SecurityType.Fund

      ) {

        return volume / 100

      }

    }

    // 北交所

    if (market == CST.Market.BSE) {

      // 北交所暂不做处理,后台转换

      return volume

    }

    return volume

  },

  // 千分位 保留digit位 isround四舍五入

  money(value: any, digit = 2, isRround = true) {

    let v = toDecimal(value, digit, isRround)

    if (v.indexOf(',') == -1) {

      v = v.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')

    }

    return v

  },

  //校验输入是否仅包含数字和字母

  isValidAlphanumeric(input: string) {

    const alphanumericPattern = /^[a-zA-Z0-9]+$/

    return alphanumericPattern.test(input)

  },

  //长度至少为6个字符,必须包含大写字母、小写字母、数字,不能包含特殊字符和汉字

  isValidPassword(password: string) {

    const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/

    return passwordPattern.test(password)

  },

  //验证手机号码

  isValidPhoneNumber(phoneNumber: string) {

    const phonePattern = /^1[3-9]\d{9}$/

    return phonePattern.test(phoneNumber)

  },

  //中文姓名,不超过5个汉字,不包含任何特殊字符或数字

  isValidChineseName(name: string) {

    const namePattern = /^[\u4e00-\u9fff]{1,5}$/

    return namePattern.test(name)

  }

}

export default common


http://www.ppmy.cn/news/1555440.html

相关文章

数据结构——ST表

ST表的定义 ST表&#xff0c;又名稀疏表&#xff0c;是一种基于倍增思想&#xff0c;用于解决可重复贡献问题的数据结构 倍增思想 这里列举一个去寻找一个区间内的最大值的例子 因为每次会将将区间增大一倍&#xff0c;所以才被称之为倍增思想 &#xff0c;这种思想十分好用…

3D 视觉定位技术:汽车零部件制造的智能变革引擎

在汽车零部件制造领域&#xff0c;传统工艺正面临着前所未有的挑战。市场对于零部件精度与生产效率近乎苛刻的要求&#xff0c;促使企业寻求突破之道。而 3D 视觉定位技术&#xff0c;为汽车零部件制造开启了精准定位与智能化生产的新纪元。 3D 视觉定位系统的核心技术原理 3…

MVVM和MVC区别

概念深入理解 MVC&#xff08;Model - View - Controller&#xff09; Model&#xff08;模型&#xff09; 它是整个架构的数据核心&#xff0c;负责数据的完整性和一致性。这包括数据的存储结构定义、数据访问逻辑&#xff08;如数据库连接、查询语句的编写&#xff09;以及数…

C语言单元总结

黑色加粗表示刷题刷到这样的题 红色加粗表示可能重要 目录 单元一 程序设计宏观认识 C语言程序框架 C语言程序构成 标识符 程序开发过程 单元二 程序设计基础知识 数据类型 常量 &#xff08;1&#xff09;直接常量 &#xff08;2&#xff09;符号常量 变量 强…

04面向对象篇(D5_Thinking(D1_Thinking - 思考))

目录 一、为什么要谈论OOA、OOD、OOP&#xff1f; 二、你知道OOA、OOD、OOD对软件工程师的重要性&#xff1f; 三、DDD、OOA、OOD、OOP和OOT到底有什么区别和联系&#xff1f; 一、为什么要谈论OOA、OOD、OOP&#xff1f; OOA&#xff08;面向对象分析&#xff09;、OOD&am…

如何在RTTHREAD 的master上获得干净的vscode-clangd体验

在用clangd开发RTT吗&#xff0c;快来试试如何简单获得清晰干净的工作区 文章目录 在用clangd开发RTT吗&#xff0c;快来试试如何简单获得清晰干净的工作区简介操作步骤第一步 安装clangd第二步 生成compile_compand.json 文件第三步 执行scons --targetvsc第四步 打开stm32f10…

【Linux-ubuntu通过USB传输程序点亮LED灯】

Linux-ubuntu通过USB传输程序点亮LED灯 一,初始化GPIO配置1.使能时钟2.其他寄存器配置 二&#xff0c;程序编译三&#xff0c;USB传输程序 一,初始化GPIO配置 1.使能时钟 使能就是一个控制信号&#xff0c;用于决定时钟信号是否能够有效的传递或者被使用&#xff0c;就像一个…

EXCEL 数据透视表基础操作

目录 1 选择数据&#xff0c;插入数据透视表 2 选择数据透视表生成位置 3 出现了数据透视表的面板 4 数据透视表的基本结构认识 4.1 交叉表/列联表 4.2 row, column, cell 一个新增的筛选器&#xff0c;就这么简单 4.3 可以只添加 rowcell/值 &#xff0c;也可以colu…