一、父组件代码:
<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