var myChart; var ws; // WebSocket 实例 // 弹窗 function alertError(title) { document.addEventListener('plusready', function() { console.log('......') }) try { plus.nativeUI.toast(title, { icon: '/static/common/toast-error.png', style: 'inline', verticalAlign: 'top' }); } catch (e) { //TODO handle the exception } } var app = new Vue({ el: '#app', data: { MA5: '', MA10: '', MA30: '', MA60: '', volMA5: '', volMA10: '', current: '1min', tabs: [{ 'label': '1分钟', 'value': '1min' }, { 'label': '5分钟', 'value': '5min' }, { 'label': '15分钟', 'value': '15min' }, { 'label': '30分钟', 'value': '30min' }, { 'label': '1小时', 'value': '60min' }, { 'label': '4小时', 'value': '4hour' }, { 'label': '1天', 'value': '1day' }, { 'label': '1个月', 'value': '1mon' }, ], category: 1, categoryList: [{ 'label': '深度', 'value': 1 }, { 'label': '实时成交', 'value': 2 }, { 'label': '简介', 'value': 3 }, ], txData: {}, //交易数据统计 buyList: [], sellList: [], dealHis: [], tokenInfo: {}, page: 1, klineData: [], // 存储K线数据 volumesData: [], depthList: {}, dates: [], period: '1min', currency_name:'', pair:'btcusdt', rid:null, }, created() { //this.getDepth() }, mounted() { const params = new URLSearchParams(window.location.search); const symbol = params.get('symbol'); const pair = params.get('pair'); this.currency_name=symbol; this.pair=pair myChart = echarts.init(document.getElementById('main')); this.draw() //this.getKline() // 连接 WebSocket 服务 this.connectWebSocket(); }, methods: { // 返回上一页 back() { uni.navigateBack() }, // 获取k线数据,生成k线 getKline() { var dataMA5 = this.calculateMA(5, this.klineData); var dataMA10 = this.calculateMA(10, this.klineData); var dataMA30 = this.calculateMA(30, this.klineData); //var dataMA60 = this.calculateMA(60, this.klineData); var volumeMA5 = this.calculateMA(5, this.volumesData); var volumeMA10 = this.calculateMA(10, this.volumesData); myChart.setOption({ tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } }, xAxis: [{ data: this.dates }, { data: this.dates }, ], series: [{ name: '日K', data: this.klineData }, { name: 'MA5', data: dataMA5 }, { name: 'MA10', data: dataMA10 }, { name: 'MA30', data: dataMA30 }, // { // name: 'MA60', // data: dataMA60 // }, { name: 'Volume', data: this.volumesData }, { name: 'VolumeMA5', data: volumeMA5 }, { name: 'VolumeMA10', data: volumeMA10 }, ] }) }, // 列表条数不足补全 addItem(list, type) { // type: 1开头加,2末尾加 list = list || []; let len = 20 - list.length; if (len > 0) { for (let i = 0; i < len; i++) { if (type == 1) { list.unshift({}) } else { list.push({}) } } } return list; }, // 获取深度数据 getDepth() { this.buyList = this.addItem(this.depthList.buyList || []); this.sellList = this.addItem(this.depthList.sellList || []); }, // 获取成交记录 getDealHis() { //this.dealHis=dealHis(); }, // 获取项目简介信息 getTokenInfo() { this.tokenInfo = tokenInfo; }, // 切换tab switchTab(val) { let _self=this //console.log(val) if (this.current == val) return; console.log("--------------------------------",this.period) this.current = val; let curPeriod = this.period; let klineUnsubUrl="market."+_self.pair+".kline."+this.period; // 取消订阅数据 let unsubKline = { event: "un_sub", channel: [klineUnsubUrl], id: _self.rid, type: "kline" }; ws.send(JSON.stringify(unsubKline)); this.period = val; //market.kline.btcusdt.1min.page let klineReqUrl = "market.kline." +_self.pair+"."+ _self.period + '.init'; // 请求对应信息的数据 //market.kline.btcusdt.1min let requestKline = { event: "req", channel: [klineReqUrl], //id: _self.rid }; //market.btcusdt.kline.1min // 订阅数据 let klineUrlPrefix = "market."+_self.pair+".kline."+this.period; let subKline = { event: "sub", channel: [klineUrlPrefix], id: _self.rid, type: "kline" }; //请求历史 K线数据 ws.send(JSON.stringify(requestKline)); //重新订阅数据 ws.send(JSON.stringify(subKline)); this.getKline() }, // 切换类目 switchCategory(val) { if (this.category == val) return; this.category = val; if (this.category == 1) { this.getDepth() } else if (this.category == 2) { this.getDealHis() } else { this.getTokenInfo() } }, // 截取数字字符串 保留precision小数 formatterNum(value, precision) { let reg = new RegExp('^\\d+(?:\\.\\d{0,' + precision + '})?') return value.toString().match(reg) }, // 计算MA calculateMA(dayCount, data) { var result = []; for (var i = 0, len = data.length; i < len; i++) { if (i < dayCount) { result.push('-'); continue; } var sum = 0; for (var j = 0; j < dayCount; j++) { sum += data[i - j][1]; } result.push((sum / dayCount).toFixed(2)); } return result; }, // 绘制(配置项) draw() { let that = this; var upColor = '#03ad91'; var downColor = '#dd345b'; var colorList = ['#c23531', '#2f4554', '#61a0a8', '#d48265', '#91c7ae', '#749f83', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3' ]; var labelFont = 'bold 12px Sans-serif'; var option = { backgroundColor: '#0d1723', title: { show: false }, legend: { show: false }, visualMap: { show: false, seriesIndex: 4, dimension: 2, pieces: [{ value: 1, color: downColor }, { value: -1, color: upColor }] }, grid: [{ top: '5%', left: 20, right: 30, height: '70%' }, { top: '80%', left: 20, right: 30, height: '16%' }, ], axisPointer: { //坐标轴指示器配置项 link: { xAxisIndex: 'all' }, label: { backgroundColor: '#0d1723', color: '#fff', borderColor: 'rgb(99, 117, 139)', borderWidth: 1, borderRadius: 2, fontSize: 10 } }, xAxis: [{ type: 'category', //坐标轴类型。(value:数值轴,适用于连续数据。,category:类目轴,适用于离散的类目数据,time: 时间轴,适用于连续的时序数据,log:对数轴。适用于对数数据) data: [], //类目数据,在类目轴(type: 'category')中有效。 scale: true, boundaryGap: false, //坐标轴两边留白策略,类目轴和非类目轴的设置和表现不一样。 axisLine: { show: false }, //坐标轴轴线相关设置 axisTick: { show: false }, //坐标轴刻度相关设置。 axisLabel: { show: false, }, //坐标轴刻度标签的相关设置。 splitLine: { show: false, lineStyle: { color: 'rgba(255,255,255, 0.1)' } }, //坐标轴在 grid 区域中的分隔线。 min: 'dataMin', //坐标轴刻度最小值。可以设置成特殊值 'dataMin',此时取数据在该轴上的最小值作为最小刻度。 max: 'dataMax', //坐标轴刻度最大值。可以设置成特殊值 'dataMax',此时取数据在该轴上的最大值作为最大刻度。 axisPointer: { //snap: true, label: { margin: 200 } }, }, { type: 'category', gridIndex: 1, //x 轴所在的 grid 的索引,默认位于第一个 grid。 data: [], //类目数据,在类目轴(type: 'category')中有效。 scale: true, boundaryGap: false, //坐标轴两边留白策略,类目轴和非类目轴的设置和表现不一样。 axisLine: { show: false, lineStyle: { color: 'rgba(255,255,255,1)', width: 1 } }, //坐标轴轴线相关设置 axisTick: { show: false }, //坐标轴刻度相关设置。 axisLabel: { //坐标轴刻度标签的相关设置。 show: true, margin: 6, fontSize: 10, color: 'rgba(99, 117, 139, 1.0)', formatter: function(value) { return echarts.format.formatTime('MM-dd', value); } }, splitNumber: 20, splitLine: { show: false, lineStyle: { color: 'rgba(255,255,255, 0.1)' } }, //坐标轴在 grid 区域中的分隔线。 min: 'dataMin', //坐标轴刻度最小值。可以设置成特殊值 'dataMin',此时取数据在该轴上的最小值作为最小刻度。 max: 'dataMax', //坐标轴刻度最大值。可以设置成特殊值 'dataMax',此时取数据在该轴上的最大值作为最大刻度。 // axisPointer: { show: true, type: 'none', label: { show: false }}, }], yAxis: [{ type: 'value', //坐标轴类型。(value:数值轴,适用于连续数据。,category:类目轴,适用于离散的类目数据,time: 时间轴,适用于连续的时序数据,log:对数轴。适用于对数数据) position: 'right', //y 轴的位置。'left','right' scale: true, //是否是脱离 0 值比例。设置成 true 后坐标刻度不会强制包含零刻度。在双数值轴的散点图中比较有用。(在设置 min 和 max 之后该配置项无效。) axisLine: { show: true }, //坐标轴轴线相关设置。 axisTick: { show: true, inside: true }, //坐标轴刻度相关设置。 axisLabel: { //坐标轴刻度标签的相关设置。 show: true, color: 'rgba(99, 117, 139, 1.0)', inside: true, fontSize: 10, formatter: function(value) { return Number(value).toFixed(2) } }, splitLine: { show: false, lineStyle: { color: 'rgba(255,255,255, 0.1)' } }, //坐标轴在 grid 区域中的分隔线。 }, { type: 'value', position: 'right', scale: true, gridIndex: 1, axisLine: { show: false }, axisTick: { show: false }, axisLabel: { show: false }, splitLine: { show: false } }], animation: false, //是否开启动画。 color: colorList, tooltip: { show: true, //是否显示提示框组件,包括提示框浮层和 axisPointer。 trigger: 'axis', //触发类型。item,axis,none formatter(params) { let tooltip = ''; let time = '', open = 0, high = 0, low = 0, close = 0, amount = 0; for (var i = 0; i < params.length; i++) { if (params[i].seriesName === '日K') { time = params[i].name; open = params[i].data.length > 1 ? Number(that.formatterNum(params[i].data[ 1], 2)) : 0; close = params[i].data.length > 1 ? Number(that.formatterNum(params[i].data[ 2], 2)) : 0; low = params[i].data.length > 1 ? Number(that.formatterNum(params[i].data[ 3], 2)) : 0; high = params[i].data.length > 1 ? Number(that.formatterNum(params[i].data[ 4], 2)) : 0; amount = params[i].data.length > 1 ? Number(that.formatterNum(params[i] .data[5], 2)) : 0; // console.log(time,open,close,low,high,amount) tooltip = '
' + '
' + '时间' + '
' + time + '
' + '
' + '开' + '
' + open + '
' + '
' + '高' + '
' + high + '
' + '
' + '低' + '
' + low + '
' + '
' + '收' + '
' + close + '
' + '
' + '数量' + '
' + amount + '
'; } if (params[i].seriesName === 'MA5') { that.MA5 = params[i].data !== 'NAN' ? Number(that.formatterNum(params[i] .data, 2)) : 0 } if (params[i].seriesName === 'MA10') { that.MA10 = params[i].data !== 'NAN' ? Number(that.formatterNum(params[i] .data, 2)) : 0 } if (params[i].seriesName === 'MA30') { that.MA30 = params[i].data !== 'NAN' ? Number(that.formatterNum(params[i] .data, 2)) : 0 } // if (params[i].seriesName === 'MA60') { // that.MA60 = params[i].data !== 'NAN' ? Number(that.formatterNum(params[i].data, 2)) : 0 // } if (params[i].seriesName === 'VolumeMA5') { that.volMA5 = params[i].data !== 'NAN' ? Number(that.formatterNum(params[i] .data, 2)) : 0 } if (params[i].seriesName === 'VolumeMA10') { that.volMA10 = params[i].data !== 'NAN' ? Number(that.formatterNum(params[i] .data, 2)) : 0 } } return tooltip; }, triggerOn: 'click', //提示框触发的条件 'mousemove','click','mousemove|click','none' textStyle: { fontSize: 10 }, //提示框浮层的文本样式 backgroundColor: 'rgba(30,42,66,0.8);', //提示框浮层的背景颜色。 borderColor: '#2f3a56', //提示框浮层的边框颜色。 borderWidth: 2, position: function(pos, params, el, elRect, size) { //提示框浮层的位置,默认不设置时位置会跟随鼠标的位置。 var obj = { top: 20 }; obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 10; return obj; }, axisPointer: { //坐标轴指示器配置项。 label: { color: 'rgba(255,255,255,.87)', fontSize: 9, backgroundColor: '#020204', borderColor: "#9c9fa4", shadowBlur: 0, borderWidth: 0.5, padding: [4, 2, 3, 2], }, animation: false, type: 'cross', lineStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(30, 42, 66, 0.1)' // 0% 处的颜色 }, { offset: 0.7, color: 'rgba(30, 42, 66,0.9)' // 100% 处的颜色 }, { offset: 1, color: 'rgba(30, 42, 66,0.2)' // 100% 处的颜色 }] }, width: 10, shadowColor: 'rgba(30, 42, 66,0.7)', shadowBlur: 0, shadowOffsetY: 68, } } }, dataZoom: [{ //用于区域缩放 type: 'inside', xAxisIndex: [0, 1], realtime: false, start: 50, end: 100, }], series: [{ type: 'candlestick', name: '日K', data: [], itemStyle: { color: upColor, color0: downColor, borderColor: upColor, borderColor0: downColor }, markPoint: { symbol: 'rect', symbolSize: [-10, 0.5], symbolOffset: [5, 0], itemStyle: { color: 'rgba(255,255,255,.87)' }, label: { color: 'rgba(255,255,255,.87)', offset: [10, 0], fontSize: 10, align: 'left', formatter: function(params) { return Number(params.value).toFixed(2) } }, data: [{ name: 'max', type: 'max', valueDim: 'highest' }, { name: 'min', type: 'min', valueDim: 'lowest' } ] }, }, { name: 'MA5', type: 'line', data: [], symbol: 'none', //去除圆点 smooth: true, lineStyle: { normal: { opacity: 1, width: 1, color: "#eef4ba" } }, z: 5 }, { name: 'MA10', type: 'line', data: [], symbol: 'none', //去除圆点 smooth: true, lineStyle: { normal: { opacity: 1, width: 1, color: '#83c1c5' } }, z: 4 }, { name: 'MA30', type: 'line', data: [], symbol: 'none', //去除圆点 smooth: true, lineStyle: { normal: { opacity: 1, width: 1, color: '#b39cd8' } }, z: 3 }, // { // name: 'MA60', // type: 'line', // data: [], // symbol: 'none',//去除圆点 // smooth: true, // lineStyle: { normal: { opacity: 1, width: 1, color: '#b39cd8' } }, // z: 1 // }, { name: 'Volume', type: 'bar', xAxisIndex: 1, yAxisIndex: 1, data: [] }, { name: 'VolumeMA5', type: 'line', xAxisIndex: 1, yAxisIndex: 1, data: [], symbol: 'none', //去除圆点 smooth: true, lineStyle: { normal: { opacity: 1, width: 1, color: "#eef4ba" } }, z: 5 }, { name: 'VolumeMA10', type: 'line', xAxisIndex: 1, yAxisIndex: 1, data: [], symbol: 'none', //去除圆点 smooth: true, lineStyle: { normal: { opacity: 1, width: 1, color: '#83c1c5' } }, z: 4 }, ] }; myChart.setOption(option); // 加载上一页数据 myChart.on('datazoom', function(params) { let num = params.batch[0]['start']; if (num == 0) { console.log('到最左边了') } }) window.addEventListener('resize', () => { myChart.resize() }) }, // 连接到 WebSocket connectWebSocket() { let _self=this; ws = new WebSocket('wss://jacqueshuang-cion.hf.space'); // 连接 WebSocket 服务端 // 动态生成频道名称 const klineReqChannel = `market.kline.${this.pair}.${this.period}.init`; const klineSubChannel = `market.${this.pair}.kline.${this.period}`; const depthChannel = `market.${this.pair}.depth.step1`; const tradeChannel = `market.${this.pair}.trade.detail`; const detailChannel = `market.${this.pair}.detail`; //请求对应信息的数据 let requestKline = { "event": "req", "channel": [klineReqChannel] //req 数组只能一个 }; //订阅k线数据 let subKline = { "event": "sub", "id": "723c9150-e143-4d80-84fc-6d0acdcba8f5", "channel": [klineSubChannel], //1min 5min 15min 30min 60min 1day 1mon.. "type": "kline" }; //订阅深度数据 let subDepth = { "event": "sub", "id": "723c9150-e143-4d80-84fc-6d0acdcba8f5", "channel": [depthChannel], "type": "depth" }; //订阅成交量 let subDetail = { "event": "sub", "id": "723c9150-e143-4d80-84fc-6d0acdcba8f5", "channel": [tradeChannel], "type": "trade" }; let subTicker = { "event": "sub", "id": "723c9150-e143-4d80-84fc-6d0acdcba8f5", "channel": [detailChannel], "type": "detail" } // WebSocket 打开时的回调 ws.onopen = () => { }; // WebSocket 收到消息时的回调 ws.onmessage = (event) => { var blob = event.data; let result = JSON.parse(blob) if (result.event == 'conn') { let id = result.data; _self.rid=id; subKline.id = id; subTicker.id = id; subDepth.id = id; subDetail.id=id; //订阅实时 K线数据 ws.send(JSON.stringify(requestKline)) // //请求历史 K线数据 ws.send(JSON.stringify(subKline)) //详细 ws.send(JSON.stringify(subTicker)) // //订阅成交量 ws.send(JSON.stringify(subDetail)); // //请求深度数据 ws.send(JSON.stringify(subDepth)); } if (result.event == 'req'&&result.channel.includes("kline")) { this.handleData(result); } if (result.event == 'kline') { this.handleData(result); } if (result.event == 'ticker') { this.handleData(result); } if (result.event == 'depth') { this.handleData(result); } if (result.event == 'detail') { this.handleData(result); } if (result.event == 'trade') { this.handleData(result); } }; // WebSocket 错误时的回调 ws.onerror = (error) => { console.error('WebSocket 错误:', error); }; // WebSocket 关闭时的回调 ws.onclose = () => { console.log('WebSocket 已关闭'); }; }, // 处理接收到的信息 async handleData(msg) { //console.log(msg); // 判断数据类型 if (msg.event && msg.channel.includes('kline')) { // K线数据 this.updateKlineData(msg.data); } else if (msg.event && msg.event.includes('depth')) { // 深度数据 this.updateDepthData(msg.data); //聚合数据 } else if (msg.event && msg.event.includes('detail')) { this.updateTickerData(msg.data); } else if (msg.event && msg.event.includes('trade')) { this.updateDetailData(msg.data); } if (msg.event && msg.event.includes('req')) { //K线数据 this.initKlineData(msg.data); } // 处理ping-pong if (msg.ping) { // 如果是 ping 消息 this.sendHeartMessage(msg.ping); } }, // 初始化K-line 图表数据 async initKlineData(res) { // 假设服务器发送的数据格式为:[时间, 开盘价, 收盘价, 最低价, 最高价, 成交量] this.klineData = res; // 提取 K-line 数据 // 转换数据 const { rawData, dates, klineData, volumesData } = this.transformData(this.klineData); //console.log("Raw Data:", rawData); this.dates = dates; this.klineData = rawData.map(function(item) { return [+item[1], +item[2], +item[3], +item[4], +item[5]]; }); this.volumesData = rawData.map(function(item, index) { return [index, item[5], item[1] > item[2] ? 1 : -1]; }); this.getKline(); }, //实时处理订阅k线数据 async updateKlineData(newKline) { console.log(newKline); const date = this.timestampToDate(newKline.KTime); // 检查时间戳是否已经存在 if (this.dates.includes(date)) { // 更新最后一条 K 线数据 const lastIndex = this.dates.length - 1; this.klineData[lastIndex] = [ newKline['open'], newKline['close'], newKline['low'], newKline['high'], Math.round(newKline['vol']) ]; this.volumesData[lastIndex] = [ lastIndex + 1, Math.round(newKline['vol']), newKline['open'] > newKline['close'] ? 1 : -1 ]; } else { // 如果是新时间戳,添加到数据中 this.dates.push(date); this.klineData.push([ newKline['open'], newKline['close'], newKline['low'], newKline['high'], Math.round(newKline['vol']) ]); this.volumesData.push([ this.klineData.length + 1, Math.round(newKline['vol']), newKline['open'] > newKline['close'] ? 1 : -1 ]); } // 更新图表 this.getKline(); }, //更新深度数据 async updateDepthData(tick) { let buyList = tick.bids.map(item => ({ price: item[0], // 买价 amount: item[1], // 买量 width: this.sum(1, 100) })); let sellList = tick.asks.map(item => ({ price: item[0], // 卖价 amount: item[1], // 卖量 width: this.sum(1, 100) })); this.depthList = { buyList: buyList, sellList: sellList }; this.getDepth(); }, // 获取24小时交易数据统计 async updateTickerData(tick) { console.log('ticker', tick); // 计算涨幅 const upRate = ((tick.close - tick.open) / tick.open * 100).toFixed(2) + "%"; // 判断涨跌 const upFlag = tick.close > tick.open ? "1" : "2"; // 1涨绿 2跌红 this.txData = { // 最新成交价 "lastPrice": tick.close, // 涨幅 "upRate": upRate, // 1涨绿 2跌红 "upFlag": upFlag, // 24小时交易量 "amount": Math.floor(tick.amount), // 24小时最高价 "high": tick.high, // 24小时最低价 "low": tick.low, //以报价币种计量的交易量(以滚动24小时计) "vol": Math.floor(tick.vol) } }, async updateDetailData(tick) { let detailList = tick.data.map(item => ({ date: this.formatTimestamp(item['ts']), // 1买入 2卖出 takerFlag: item['direction'] == "buy" ? "1" : "2", price: item['price'], amount: item['amount'] })); // 合并数组 this.dealHis = this.dealHis.concat(detailList); // 按时间降序排序(最新时间排在前面) this.dealHis.sort((a, b) => { // 将时间字符串转为秒数 const timeToSeconds = (time) => { const [hours, minutes, seconds] = time.split(':').map(Number); return hours * 3600 + minutes * 60 + seconds; }; return timeToSeconds(b.date) - timeToSeconds(a.date); }); }, //时间戳转时分秒 formatTimestamp(timestamp) { // 创建一个新的Date对象,参数是时间戳(毫秒) const date = new Date(timestamp); // 获取小时、分钟、秒和毫秒,并确保它们都是两位数 let hh = date.getHours().toString().padStart(2, '0'); let mm = date.getMinutes().toString().padStart(2, '0'); let ss = date.getSeconds().toString().padStart(2, '0'); //let ms = date.getMilliseconds().toString().padStart(3, '0'); // 返回格式化后的字符串 return `${hh}:${mm}:${ss}`; }, // 时间戳转换为 yyyy-mm-dd 格式 timestampToDate(timestamp) { const date = new Date(timestamp * 1000); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); // 添加小时和分钟 const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; }, // 转换火币数据为目标格式 transformData(huobiData) { if (!Array.isArray(huobiData)) { console.error("huobiData 不是一个有效的数组"); return { rawData: [], dates: [], klineData: [], volumes: [] }; } const rawData = huobiData.map(item => { const date = this.timestampToDate(item.KTime); return [date, item.open, item.close, item.low, item.high, Math.round(item.vol)]; }); // 提取 dates const dates = rawData.map(item => item[0]); // 提取 kline 数据(价格和成交量) /// /// const klineData = rawData.map(item => [+item[1], +item[2], +item[3], +item[4], +item[5]]); // 提取成交量数据 const volumes = rawData.map((item, index) => { return [index, item[5], item[1] > item[2] ? 1 : -1]; }); // console.log( { // rawData, // dates, // klineData, // volumes // }) return { rawData, dates, klineData, volumes }; }, //心跳信息 sendHeartMessage(ping) { ws.send(JSON.stringify({ "pong": ping })); }, // 获取指定区间随机数 sum(m, n) { var num = Math.floor(Math.random() * (m - n) + n); return num; } } })