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

一、父组件代码:

<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

相关推荐
明月清风徐徐几秒前
Vue实训---6-完成用户退出操作
javascript·vue.js·elementui
亦世凡华、7 分钟前
从模型到视图:如何用 .NET Core MVC 构建完整 Web 应用
前端·经验分享·c#·mvc·.netcore
恋猫de小郭29 分钟前
Flutter Web 正式移除 HTML renderer,只支持 CanvasKit 和 SkWasm
前端·flutter·html
杨荧1 小时前
【开源免费】基于Vue和SpringBoot的图书进销存管理系统(附论文)
前端·javascript·vue.js·spring boot·spring cloud·java-ee·开源
:mnong1 小时前
从EXCEL表格到WEB TABLE的实践
前端
○陈2 小时前
js面试题|[2024-12-10]
java·前端·javascript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_044】5.1 饼图和环形图的创建(四):数据标签的添加
前端·javascript·信息可视化·数据可视化·d3.js·d3环形图·d3饼图
Eric_见嘉2 小时前
翻遍 Vite 文档,教你面试官都不会的 3 种前端部署方案
运维·前端·serverless
浪遏2 小时前
对比学习 apply, call, 和 bind 👀👀👀
前端·javascript·面试
常常不爱学习2 小时前
CSS元素宽高特点、类型转化、显式和隐藏(display)
前端·javascript·css