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
> <span class="color1"> DEA:</span>
<span>{{ KHoverData[9] }}</span
> <span class="color2"> DIF:</span>
<span>{{ KHoverData[10] }}</span
>
</p>
</div>
<div
v-if="activeIndex !== 0 && currentIndex === 3"
class="pos-box macd-box"
>
<p>
<span class="color1">K:</span>
<span>{{ KHoverData[13] }}</span
> <span class="color2">D:</span>
<span>{{ KHoverData[11] }}</span
> <span class="color3">J:</span>
<span>{{ KHoverData[12] }}</span
>
</p>
</div>
<div
v-if="activeIndex !== 0 && currentIndex === 4"
class="pos-box macd-box"
>
<p>
<span class="color1">RSI6:</span>
<span>{{ KHoverData[14] }}</span
> <span class="color2">RSI12:</span>
<span>{{ KHoverData[15] }}</span
> <span class="color3">RSI24:</span>
<span>{{ KHoverData[16] }}</span
>
</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
相关文章:
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…...
小程序开发实战项目:构建简易待办事项列表
随着移动互联网的飞速发展,小程序以其便捷性、即用即走的特点,成为了连接用户与服务的重要桥梁。无论是电商平台的购物助手,还是餐饮行业的点餐系统,小程序都在各个领域发挥着巨大的作用。 小程序开发基础 1. 小程序简介 小程序是…...
SD Express 卡漏洞导致笔记本电脑和游戏机遭受内存攻击
Positive Technologies 最近发布的一份报告揭示了一个名为 DaMAgeCard 的新漏洞,攻击者可以利用该漏洞利用 SD Express 内存卡直接访问系统内存。 该漏洞利用了 SD Express 中引入的直接内存访问 (DMA) 功能来加速数据传输速度,但也为对支持该标准的设备…...
前端node环境安装:nvm安装详细教程(安装nvm、node、npm、cnpm、yarn及环境变量配置)
需求:在做前端开发的时候,有的时候 这个项目需要 node 14 那个项目需要 node 16,我们也不能卸载 安装 。这岂不是很麻烦。这个时候 就需要 一个工具 来管理我们的 node 版本和 npm 版本。 下面就分享一个 nvm 工具 用来管理 node 版本。 这个…...
java之集合(详细-Map,Set,List)
1集合体系概述 1.1集合的概念 集合是一种容器,用来装数据的,类似于数组,但集合的大小可变,开发中也非常常用。 1.2集合分类 集合分为单列集合和多列集合 Collection代表单列集合,每个元素(数据ÿ…...
常见LeetCode-Saw200
用来记录需要知道见过的题型: LeetCode2-两数相加 说明:以链表的形势给了你每个位的数字,而且是逆序,直接从开头(个位)遍历相加。带上进位即可。有一个为空就直接计算另一个和进位。 LeetCode-3.无重复字符…...
Unity 制作一个视频播放器(打包后,可在外部编辑并放置新的视频)
效果展示: 在这里,我把视频名称(Json)和对应的视频资源都放在了StreamingAssets文件夹下,以便于打包后,客户还可以自己在外部增加、删除、修改对应的视频资料。 如有需要,请联细抠抠。...
MySQL-SQL语句
文章目录 一. SQL语句介绍二. SQL语句分类1. 数据定义语言:简称DDL(Data Definition Language)2. 数据操作语言:简称DML(Data Manipulation Language)3. 数据查询语言:简称DQL(Data Query Language)4. 数据控制语言:简称DCL(Data …...
腾讯微信大数据面试题及参考答案
DNS 协议是否使用 UDP? DNS(Domain Name System)协议主要使用 UDP(User Datagram Protocol),但也会使用 TCP(Transmission Control Protocol)。 UDP 是一种无连接的传输协议,它的特点是简单、高效。DNS 在进行域名解析时,大部分情况下使用 UDP。因为 UDP 的开销小,对…...
Python跳动的爱心
系列文章 序号直达链接表白系列1Python制作一个无法拒绝的表白界面2Python满屏飘字表白代码3Python无限弹窗满屏表白代码4Python李峋同款可写字版跳动的爱心5Python流星雨代码6Python漂浮爱心代码7Python爱心光波代码8Python普通的玫瑰花代码9Python炫酷的玫瑰花代码10Python多…...
计算机启动过程 | Linux 启动流程
注:本文为“计算机启动、 Linux 启动”相关文章合辑。 替换引文部分不清晰的图。 探索计算机的启动过程 Aleksandr Goncharov 2023/04/21 很多人对计算机的启动方式很感兴趣。只要设备开启,这就是魔法开始和持续的地方。在本文中,我们将概…...
反射简单介绍
反射就是从类里拿东西 有的人可能会想为什么不能用io流,从上往下一行一行的读也能获取类中的信息,为什么要用反射呢? 假如我们io流,从左到右一行一行的读取数据,如果碰到局部变量和成员变量同名,怎么区分&a…...
工具篇--GitHub Desktop 使用
文章目录 前言一、GitHub Desktop 的使用:1.1 通过官网下载GitHub Desktop和安装:1.2 安装和使用:1.2.1 填充自己的标识:1.2.3 克隆项目:1.2.4 git 常用忽略项配置: 二、代码的更新和提交:2.1 代…...
单臂路由配置
知识点 单臂路由指在路由器上的一个接口配置子接口(逻辑接口)来实现不同vlan间通信 路由器上的每个物理接口都可以配置多个子接口(逻辑接口) 公司的财务部、技术部和业务部有多台计算机,它们使用一台二层交换机进行互…...
河工oj第七周补题题解2024
A.GO LecturesⅠ—— Victory GO LecturesⅠ—— Victory - 问题 - 软件学院OJ 代码 统计 #include<bits/stdc.h> using namespace std;double b, w;int main() {for(int i 1; i < 19; i ) {for(int j 1; j < 19; j ) {char ch; cin >> ch;if(ch B) b …...
卷积的数学原理与作用
一、一维卷积 (一)定义 数学定义 给定一个输入序列 x [ x 1 , x 2 , ⋯ , x n ] x [x_1,x_2,\cdots,x_n] x[x1,x2,⋯,xn] 和一个卷积核(滤波器) k [ k 1 , k 2 , ⋯ , k m ] k [k_1,k_2,\cdots,k_m] k[k1,k2,⋯,…...
路由介绍.
RIB和FIB Routing Information Base(RIB),即路由信息库,是存储在路由器或联网计算机中的一个电子表格或类数据库,它保存着指向特定网络地址的路径信息,包括路径的路由度量值。RIB的主要目标是实现路由协议…...
CTFshow-命令执行(Web29-40)
CTFshow-命令执行(Web29-40) CTFWeb-命令执行漏洞过滤的绕过姿势_绕过空格过滤-CSDN博客 总结rce(远程代码执行各种sao姿势)绕过bypass_远程命令执行绕过-CSDN博客 对比两者的源代码,我们发现,cat指令把flag.php的内容导出后依…...
MySQL锁的类型有哪些
目录 共享锁(share lock): 排他锁(exclusivelock): 表锁(table lock): 行锁: 记录锁(Record lock): 页锁: 间隙锁: 基于锁的属性分类:共享锁,排他锁。 基于锁的粒…...
基于 JNI + Rust 实现一种高性能 Excel 导出方案(下篇)
衡量一个人是否幸福,不应看他有多少高兴的事,而应看他是否为小事烦扰。只有幸福的人,才会把无关痛痒的小事挂心上。那些真正经历巨大灾难和深重痛苦的人,根本无暇顾及这些小事的。因此人们往往在失去幸福之后,才会发现…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
如何将联系人从 iPhone 转移到 Android
从 iPhone 换到 Android 手机时,你可能需要保留重要的数据,例如通讯录。好在,将通讯录从 iPhone 转移到 Android 手机非常简单,你可以从本文中学习 6 种可靠的方法,确保随时保持连接,不错过任何信息。 第 1…...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...
华为OD机考-机房布局
import java.util.*;public class DemoTest5 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseSystem.out.println(solve(in.nextLine()));}}priv…...
