feat: 更新 @riwa/api-types 依赖版本至 0.0.138;重构 WebSocket 逻辑以优化订阅和取消订阅功能;调整订单簿组件以支持新的数据结构
This commit is contained in:
@@ -35,7 +35,7 @@
|
|||||||
"@elysiajs/eden": "^1.4.5",
|
"@elysiajs/eden": "^1.4.5",
|
||||||
"@ionic/vue": "^8.7.11",
|
"@ionic/vue": "^8.7.11",
|
||||||
"@ionic/vue-router": "^8.7.11",
|
"@ionic/vue-router": "^8.7.11",
|
||||||
"@riwa/api-types": "http://192.168.1.7:9528/api/riwa-eden-0.0.135.tgz",
|
"@riwa/api-types": "http://192.168.1.7:9528/api/riwa-eden-0.0.138.tgz",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -69,8 +69,8 @@ importers:
|
|||||||
specifier: ^8.7.11
|
specifier: ^8.7.11
|
||||||
version: 8.7.11(@stencil/core@4.39.0)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
version: 8.7.11(@stencil/core@4.39.0)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
||||||
'@riwa/api-types':
|
'@riwa/api-types':
|
||||||
specifier: http://192.168.1.7:9528/api/riwa-eden-0.0.135.tgz
|
specifier: http://192.168.1.7:9528/api/riwa-eden-0.0.138.tgz
|
||||||
version: '@riwa/eden@http://192.168.1.7:9528/api/riwa-eden-0.0.135.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
|
version: '@riwa/eden@http://192.168.1.7:9528/api/riwa-eden-0.0.138.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.18
|
specifier: ^4.1.18
|
||||||
version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
|
version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
|
||||||
@@ -2804,9 +2804,9 @@ packages:
|
|||||||
'@remirror/core-constants@3.0.0':
|
'@remirror/core-constants@3.0.0':
|
||||||
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
|
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
|
||||||
|
|
||||||
'@riwa/eden@http://192.168.1.7:9528/api/riwa-eden-0.0.135.tgz':
|
'@riwa/eden@http://192.168.1.7:9528/api/riwa-eden-0.0.138.tgz':
|
||||||
resolution: {tarball: http://192.168.1.7:9528/api/riwa-eden-0.0.135.tgz}
|
resolution: {tarball: http://192.168.1.7:9528/api/riwa-eden-0.0.138.tgz}
|
||||||
version: 0.0.135
|
version: 0.0.138
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@elysiajs/eden': ^1.4.5
|
'@elysiajs/eden': ^1.4.5
|
||||||
|
|
||||||
@@ -12161,7 +12161,7 @@ snapshots:
|
|||||||
|
|
||||||
'@remirror/core-constants@3.0.0': {}
|
'@remirror/core-constants@3.0.0': {}
|
||||||
|
|
||||||
'@riwa/eden@http://192.168.1.7:9528/api/riwa-eden-0.0.135.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
'@riwa/eden@http://192.168.1.7:9528/api/riwa-eden-0.0.138.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@elysiajs/eden': 1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))
|
'@elysiajs/eden': 1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||||
|
|
||||||
|
|||||||
@@ -205,18 +205,14 @@ export class RWADatafeed extends Datafeeds.UDFCompatibleDatafeed {
|
|||||||
console.log("[RWADatafeed]: subscribeBars", { symbol: symbolInfo.name, resolution, subscriberUID });
|
console.log("[RWADatafeed]: subscribeBars", { symbol: symbolInfo.name, resolution, subscriberUID });
|
||||||
|
|
||||||
this.subscribers.set(subscriberUID, { callback: onTick, resolution, symbol: symbolInfo.name });
|
this.subscribers.set(subscriberUID, { callback: onTick, resolution, symbol: symbolInfo.name });
|
||||||
const wsConnection = tradeWebSocket.getSocket();
|
|
||||||
|
|
||||||
wsConnection.send({
|
tradeWebSocket.subscribeChannel([{
|
||||||
action: "subscribe",
|
|
||||||
channels: [{
|
|
||||||
name: "bar",
|
name: "bar",
|
||||||
symbol: symbolInfo.name,
|
symbol: symbolInfo.name,
|
||||||
resolution,
|
resolution,
|
||||||
}],
|
}]);
|
||||||
});
|
|
||||||
|
|
||||||
wsConnection.subscribe((message) => {
|
tradeWebSocket.subscribe((message) => {
|
||||||
const data = message.data as any;
|
const data = message.data as any;
|
||||||
if (data.type !== "bar")
|
if (data.type !== "bar")
|
||||||
return;
|
return;
|
||||||
@@ -238,13 +234,10 @@ export class RWADatafeed extends Datafeeds.UDFCompatibleDatafeed {
|
|||||||
unsubscribeBars(subscriberUID: string): void {
|
unsubscribeBars(subscriberUID: string): void {
|
||||||
console.log("[RWADatafeed]: unsubscribeBars", subscriberUID);
|
console.log("[RWADatafeed]: unsubscribeBars", subscriberUID);
|
||||||
const subscriber = this.subscribers.get(subscriberUID);
|
const subscriber = this.subscribers.get(subscriberUID);
|
||||||
const wsConnection = tradeWebSocket.getSocket();
|
tradeWebSocket.unsubscribeChannel([{
|
||||||
wsConnection.send({
|
|
||||||
action: "unsubscribe",
|
|
||||||
channels: [{
|
|
||||||
name: "bar",
|
name: "bar",
|
||||||
symbol: subscriber?.symbol || "",
|
symbol: subscriber?.symbol as string,
|
||||||
}],
|
resolution: subscriber?.resolution as ResolutionString,
|
||||||
});
|
}]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,364 @@
|
|||||||
|
import type { Treaty } from "@elysiajs/eden";
|
||||||
import type { MarketDataStreaming } from "@/api/types";
|
import type { MarketDataStreaming } from "@/api/types";
|
||||||
import { client } from "@/api";
|
import { client } from "@/api";
|
||||||
|
|
||||||
|
interface SubscribeMessage {
|
||||||
|
action: "subscribe" | "unsubscribe" | "ping";
|
||||||
|
channels: Array<{
|
||||||
|
name: "depth" | "trades" | "ticker" | "bar";
|
||||||
|
symbol: string;
|
||||||
|
resolution?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageData = any;
|
||||||
|
|
||||||
|
interface MessageHandler {
|
||||||
|
id: string;
|
||||||
|
handler: (data: MessageData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export class TradeWebSocket {
|
export class TradeWebSocket {
|
||||||
public socket: MarketDataStreaming | null = null;
|
private socket: MarketDataStreaming | null = null;
|
||||||
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
private reconnectAttempts = 0;
|
||||||
|
private maxReconnectAttempts = 5;
|
||||||
|
private reconnectDelay = 3000;
|
||||||
|
private heartbeatInterval = 30000; // 30秒心跳
|
||||||
|
private isManualClose = false;
|
||||||
|
private connectionPromise: Promise<void> | null = null;
|
||||||
|
private connectionResolve: (() => void) | null = null;
|
||||||
|
private messageQueue: SubscribeMessage[] = [];
|
||||||
|
private messageHandlers: MessageHandler[] = [];
|
||||||
|
private subscriptions = new Set<string>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!this.socket) {
|
this.connect();
|
||||||
this.socket = client.api.market_data.streaming.subscribe();
|
|
||||||
}
|
}
|
||||||
this.socket.on("error", () => {
|
|
||||||
this.socket = null;
|
/**
|
||||||
this.socket = client.api.market_data.streaming.subscribe();
|
* 连接 WebSocket
|
||||||
|
*/
|
||||||
|
private connect() {
|
||||||
|
if (this.socket) {
|
||||||
|
return this.connectionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建连接 Promise
|
||||||
|
this.connectionPromise = new Promise((resolve) => {
|
||||||
|
this.connectionResolve = resolve;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.socket = client.api.market_data.streaming.subscribe();
|
||||||
|
|
||||||
this.socket.on("open", () => {
|
this.socket.on("open", () => {
|
||||||
console.log("TradeWebSocket connected");
|
console.log("✅ TradeWebSocket connected");
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.startHeartbeat();
|
||||||
|
|
||||||
|
// 连接成功,通知等待的请求
|
||||||
|
if (this.connectionResolve) {
|
||||||
|
this.connectionResolve();
|
||||||
|
this.connectionResolve = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理消息队列
|
||||||
|
this.processMessageQueue();
|
||||||
|
|
||||||
|
// 重新订阅之前的频道
|
||||||
|
this.resubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("message", (data) => {
|
||||||
|
this.handleMessage(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("error", (error: any) => {
|
||||||
|
console.error("❌ TradeWebSocket error:", error);
|
||||||
|
this.handleError();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("close", () => {
|
||||||
|
console.log("🔌 TradeWebSocket closed");
|
||||||
|
this.stopHeartbeat();
|
||||||
|
this.socket = null;
|
||||||
|
this.connectionPromise = null;
|
||||||
|
|
||||||
|
if (!this.isManualClose) {
|
||||||
|
this.reconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("❌ Failed to create WebSocket:", error);
|
||||||
|
this.handleError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.connectionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理接收到的消息
|
||||||
|
*/
|
||||||
|
private handleMessage(data: any) {
|
||||||
|
// 处理心跳响应
|
||||||
|
if (data.type === "pong") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分发消息给所有订阅者
|
||||||
|
this.messageHandlers.forEach((handler) => {
|
||||||
|
try {
|
||||||
|
handler.handler(data);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("❌ Message handler error:", error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSocket(): MarketDataStreaming {
|
/**
|
||||||
return this.socket!;
|
* 错误处理
|
||||||
|
*/
|
||||||
|
private handleError() {
|
||||||
|
this.stopHeartbeat();
|
||||||
|
this.socket = null;
|
||||||
|
this.connectionPromise = null;
|
||||||
|
|
||||||
|
if (!this.isManualClose) {
|
||||||
|
this.reconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重连逻辑
|
||||||
|
*/
|
||||||
|
private reconnect() {
|
||||||
|
if (this.reconnectTimer || this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
console.error("❌ Max reconnect attempts reached");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
const delay = this.reconnectDelay * Math.min(this.reconnectAttempts, 5);
|
||||||
|
|
||||||
|
console.log(`🔄 Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
||||||
|
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
this.connect();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动心跳
|
||||||
|
*/
|
||||||
|
private startHeartbeat() {
|
||||||
|
this.stopHeartbeat();
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
if (this.socket && this.isConnected()) {
|
||||||
|
try {
|
||||||
|
this.socket.send({ action: "ping" });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("❌ Heartbeat error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, this.heartbeatInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止心跳
|
||||||
|
*/
|
||||||
|
private stopHeartbeat() {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer);
|
||||||
|
this.heartbeatTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理消息队列
|
||||||
|
*/
|
||||||
|
private processMessageQueue() {
|
||||||
|
if (!this.isConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (this.messageQueue.length > 0) {
|
||||||
|
const message = this.messageQueue.shift();
|
||||||
|
if (message) {
|
||||||
|
this.sendImmediate(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新订阅所有频道
|
||||||
|
*/
|
||||||
|
private resubscribe() {
|
||||||
|
if (this.subscriptions.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channels = Array.from(this.subscriptions).map((sub) => {
|
||||||
|
const [name, symbol] = sub.split(":") as any[];
|
||||||
|
return { name, symbol };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (channels.length > 0) {
|
||||||
|
this.send({
|
||||||
|
action: "subscribe",
|
||||||
|
channels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已连接
|
||||||
|
*/
|
||||||
|
public isConnected(): boolean {
|
||||||
|
return this.socket !== null && this.connectionResolve === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待连接建立
|
||||||
|
*/
|
||||||
|
public async waitForConnection(): Promise<void> {
|
||||||
|
if (this.isConnected()) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.connectionPromise) {
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.connectionPromise!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息(立即发送)
|
||||||
|
*/
|
||||||
|
private sendImmediate(message: SubscribeMessage) {
|
||||||
|
if (!this.socket) {
|
||||||
|
console.warn("⚠️ Socket not connected, message queued");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.socket.send(message);
|
||||||
|
|
||||||
|
// 记录订阅状态
|
||||||
|
if (message.action === "subscribe") {
|
||||||
|
message.channels.forEach((channel) => {
|
||||||
|
const key = channel.symbol ? `${channel.name}:${channel.symbol}` : channel.name;
|
||||||
|
this.subscriptions.add(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (message.action === "unsubscribe") {
|
||||||
|
message.channels.forEach((channel) => {
|
||||||
|
const key = channel.symbol ? `${channel.name}:${channel.symbol}` : channel.name;
|
||||||
|
this.subscriptions.delete(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("❌ Failed to send message:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息(带队列)
|
||||||
|
*/
|
||||||
|
public async send(message: SubscribeMessage) {
|
||||||
|
// 等待连接建立
|
||||||
|
await this.waitForConnection();
|
||||||
|
|
||||||
|
// 如果已连接,立即发送
|
||||||
|
if (this.isConnected()) {
|
||||||
|
this.sendImmediate(message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 否则加入队列
|
||||||
|
this.messageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅消息
|
||||||
|
*/
|
||||||
|
public subscribe(handler: (data: MessageData) => void): string {
|
||||||
|
const id = `handler_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
this.messageHandlers.push({ id, handler });
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅消息
|
||||||
|
*/
|
||||||
|
public unsubscribe(id: string) {
|
||||||
|
const index = this.messageHandlers.findIndex(h => h.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.messageHandlers.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅频道
|
||||||
|
*/
|
||||||
|
public async subscribeChannel(channels: SubscribeMessage["channels"]) {
|
||||||
|
await this.send({
|
||||||
|
action: "subscribe",
|
||||||
|
channels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅频道
|
||||||
|
*/
|
||||||
|
public async unsubscribeChannel(channels: SubscribeMessage["channels"]) {
|
||||||
|
await this.send({
|
||||||
|
action: "unsubscribe",
|
||||||
|
channels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭连接
|
||||||
|
*/
|
||||||
public closeSocket() {
|
public closeSocket() {
|
||||||
|
this.isManualClose = true;
|
||||||
|
this.stopHeartbeat();
|
||||||
|
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer);
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.connectionPromise = null;
|
||||||
|
this.messageQueue = [];
|
||||||
|
this.subscriptions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置并重新连接
|
||||||
|
*/
|
||||||
|
public reset() {
|
||||||
|
this.isManualClose = false;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.closeSocket();
|
||||||
|
this.connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Treaty } from "@elysiajs/eden";
|
|
||||||
import { client, safeClient } from "@/api";
|
|
||||||
import { tradeWebSocket } from "@/tradingview/websocket";
|
import { tradeWebSocket } from "@/tradingview/websocket";
|
||||||
|
|
||||||
type Item = Treaty.Data<typeof client.api.trading_pairs.orderbook.get>;
|
interface Item {
|
||||||
|
asks: Array<{
|
||||||
|
price: string;
|
||||||
|
size: string;
|
||||||
|
}>;
|
||||||
|
bids: Array<{
|
||||||
|
price: string;
|
||||||
|
size: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}>();
|
}>();
|
||||||
const socket = tradeWebSocket.getSocket();
|
|
||||||
|
|
||||||
const { data } = await safeClient(client.api.trading_pairs.orderbook.get({ query: { symbol: props.symbol, depth: 5 } }));
|
const data = ref<Item>({
|
||||||
|
asks: [],
|
||||||
|
bids: [],
|
||||||
|
});
|
||||||
|
|
||||||
const latestPrice = computed(() => {
|
const latestPrice = computed(() => {
|
||||||
if (!data.value || !data.value.asks?.length || !data.value.bids?.length) {
|
if (!data.value || !data.value.asks?.length || !data.value.bids?.length) {
|
||||||
@@ -22,41 +31,44 @@ const latestPrice = computed(() => {
|
|||||||
return price;
|
return price;
|
||||||
});
|
});
|
||||||
|
|
||||||
function getTotal(item: Item["asks" | "bids"][number]) {
|
|
||||||
const price = Number(item.price);
|
|
||||||
const quantity = Number(item.quantity);
|
|
||||||
return (price * quantity).toFixed(2);
|
|
||||||
}
|
|
||||||
function getDepthPercent(items?: Item["asks" | "bids"], index?: number) {
|
function getDepthPercent(items?: Item["asks" | "bids"], index?: number) {
|
||||||
if (!items || index === undefined)
|
if (!items || index === undefined)
|
||||||
return 0;
|
return 0;
|
||||||
const totalValues = items.map(item => Number(item.price) * Number(item.quantity));
|
const totalValues = items.map(item => Number(item.price) * Number(item.size));
|
||||||
const maxTotal = Math.max(...totalValues);
|
const maxTotal = Math.max(...totalValues);
|
||||||
const itemTotal = totalValues[index];
|
const itemTotal = totalValues[index];
|
||||||
return (itemTotal / maxTotal) * 100;
|
return (itemTotal / maxTotal) * 100;
|
||||||
}
|
}
|
||||||
function subscribe() {
|
function subscribe() {
|
||||||
socket.send({
|
tradeWebSocket.subscribeChannel([{
|
||||||
action: "subscribe",
|
|
||||||
channels: [{
|
|
||||||
name: "depth",
|
name: "depth",
|
||||||
symbol: props.symbol,
|
symbol: props.symbol,
|
||||||
}],
|
}]);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
function unsubscribe() {
|
function unsubscribe() {
|
||||||
socket.send({
|
useTimeoutFn(() => {
|
||||||
action: "unsubscribe",
|
console.log("unsubscribe order book: ", props.symbol);
|
||||||
channels: [{
|
tradeWebSocket.unsubscribeChannel([{
|
||||||
name: "depth",
|
name: "depth",
|
||||||
symbol: props.symbol,
|
symbol: props.symbol,
|
||||||
}],
|
}]);
|
||||||
});
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听订单簿更新
|
// 监听订单簿更新
|
||||||
socket.subscribe((message) => {
|
tradeWebSocket.subscribe((message) => {
|
||||||
const data = message.data;
|
if ("bids" in message.data) {
|
||||||
|
if (!data.value?.bids.length) {
|
||||||
|
data.value!.bids = [];
|
||||||
|
}
|
||||||
|
data.value!.bids.splice(0, data.value!.bids.length, ...message.data.bids);
|
||||||
|
}
|
||||||
|
if ("asks" in message.data) {
|
||||||
|
if (!data.value?.asks.length) {
|
||||||
|
data.value!.asks = [];
|
||||||
|
}
|
||||||
|
data.value!.asks.splice(0, data.value!.asks.length, ...message.data.asks);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.symbol, (newSymbol, oldSymbol) => {
|
watch(() => props.symbol, (newSymbol, oldSymbol) => {
|
||||||
@@ -90,8 +102,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<div class="order-content">
|
<div class="order-content">
|
||||||
<span class="price text-danger-500">{{ Number(ask.price).toString() }}</span>
|
<span class="price text-danger-500">{{ Number(ask.price).toString() }}</span>
|
||||||
<span class="amount">{{ Number(ask.quantity).toString() }}</span>
|
<span class="amount">{{ ask.size }}</span>
|
||||||
<span class="total">{{ getTotal(ask) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,8 +132,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<div class="order-content">
|
<div class="order-content">
|
||||||
<span class="price text-success-500">{{ Number(bid.price).toString() }}</span>
|
<span class="price text-success-500">{{ Number(bid.price).toString() }}</span>
|
||||||
<span class="amount">{{ Number(bid.quantity).toString() }}</span>
|
<span class="amount">{{ Number(bid.size).toString() }}</span>
|
||||||
<span class="total">{{ getTotal(bid) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,16 +41,32 @@ const [form, reset] = useResetRef<SpotOrderBody & { amount: string }>({
|
|||||||
amount: "",
|
amount: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => [form.value.symbol, form.value.orderType, form.value.side], async ([symbol, type, side]) => {
|
||||||
|
const { data } = await safeClient(client.api.market_data.pairs.price_preview.get({
|
||||||
|
query: { symbol },
|
||||||
|
}));
|
||||||
|
if (type === TradeWayValueEnum.LIMIT) {
|
||||||
|
form.value.price = Number((side === TradeTypeEnum.BUY ? data.value?.ask : data.value?.bid) || data.value?.mid || "").toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
form.value.price = "";
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
async function openTradePairs() {
|
async function openTradePairs() {
|
||||||
const modal = await modalController.create({
|
const modal = await modalController.create({
|
||||||
component: TradePairsModal,
|
component: TradePairsModal,
|
||||||
|
componentProps: {
|
||||||
|
onSelect: (val) => {
|
||||||
|
symbol.value = val;
|
||||||
|
form.value.symbol = val;
|
||||||
|
},
|
||||||
|
},
|
||||||
breakpoints: [0, 0.95],
|
breakpoints: [0, 0.95],
|
||||||
initialBreakpoint: 0.95,
|
initialBreakpoint: 0.95,
|
||||||
handle: true,
|
handle: true,
|
||||||
});
|
});
|
||||||
await modal.present();
|
await modal.present();
|
||||||
const { data: result } = await modal.onWillDismiss<string>();
|
|
||||||
result && (symbol.value = result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 价格变化时,根据数量计算金额
|
// 价格变化时,根据数量计算金额
|
||||||
|
|||||||
Reference in New Issue
Block a user