feat: Add Hyperliquid trading monitoring page and API
- Add hyperliquid API with account/positions/orders endpoints - Add hyperliquid.html monitoring page - Add position close and order cancel actions - Auto-refresh every 30 seconds - Display account value, leverage, drawdown, positions, and orders
This commit is contained in:
parent
d826c6685f
commit
cb6ab397fe
383
backend/app/api/hyperliquid.py
Normal file
383
backend/app/api/hyperliquid.py
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
"""
|
||||||
|
Hyperliquid 交易 API
|
||||||
|
提供 Hyperliquid 实盘交易数据接口
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.services.hyperliquid_trading_service import get_hyperliquid_service
|
||||||
|
from app.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/hyperliquid", tags=["Hyperliquid"])
|
||||||
|
|
||||||
|
|
||||||
|
class AccountStatusResponse(BaseModel):
|
||||||
|
"""账户状态响应"""
|
||||||
|
success: bool
|
||||||
|
enabled: bool
|
||||||
|
message: str
|
||||||
|
data: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/account")
|
||||||
|
async def get_account_status():
|
||||||
|
"""
|
||||||
|
获取 Hyperliquid 账户状态
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- account_value: 账户总价值
|
||||||
|
- available_balance: 可用余额
|
||||||
|
- total_margin_used: 已用保证金
|
||||||
|
- total_position_value: 总持仓价值
|
||||||
|
- current_total_leverage: 当前总杠杆
|
||||||
|
- max_total_leverage: 最大总杠杆限制
|
||||||
|
- initial_balance: 初始余额(用于计算回撤)
|
||||||
|
- drawdown_percent: 回撤百分比
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
service = get_hyperliquid_service()
|
||||||
|
|
||||||
|
if service is None:
|
||||||
|
return AccountStatusResponse(
|
||||||
|
success=True,
|
||||||
|
enabled=False,
|
||||||
|
message="Hyperliquid 服务未启用。请在 .env 中设置 hyperliquid_trading_enabled=true",
|
||||||
|
data=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取账户状态
|
||||||
|
state = service.get_account_state()
|
||||||
|
|
||||||
|
# 计算回撤
|
||||||
|
if service.initial_balance and service.initial_balance > 0:
|
||||||
|
drawdown = (service.initial_balance - state["account_value"]) / service.initial_balance
|
||||||
|
else:
|
||||||
|
drawdown = 0
|
||||||
|
|
||||||
|
# 获取持仓
|
||||||
|
positions = service.get_open_positions()
|
||||||
|
total_position_value = sum(
|
||||||
|
abs(pos["size"]) * pos["entry_price"]
|
||||||
|
for pos in positions
|
||||||
|
)
|
||||||
|
|
||||||
|
# 计算当前总杠杆
|
||||||
|
if state["account_value"] > 0:
|
||||||
|
current_total_leverage = total_position_value / state["account_value"]
|
||||||
|
else:
|
||||||
|
current_total_leverage = 0
|
||||||
|
|
||||||
|
return AccountStatusResponse(
|
||||||
|
success=True,
|
||||||
|
enabled=True,
|
||||||
|
message="Hyperliquid 服务正常",
|
||||||
|
data={
|
||||||
|
"account_value": state["account_value"],
|
||||||
|
"available_balance": state["available_balance"],
|
||||||
|
"total_margin_used": state["total_margin_used"],
|
||||||
|
"total_position_value": total_position_value,
|
||||||
|
"current_total_leverage": current_total_leverage,
|
||||||
|
"max_total_leverage": service.max_total_leverage,
|
||||||
|
"initial_balance": service.initial_balance,
|
||||||
|
"drawdown_percent": drawdown * 100,
|
||||||
|
"wallet_address": service.wallet_address,
|
||||||
|
"leverage_limit": 10.0, # Hyperliquid 强制限制
|
||||||
|
"circuit_breaker_threshold": service.circuit_breaker_drawdown * 100
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取 Hyperliquid 账户状态失败: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/positions")
|
||||||
|
async def get_positions(
|
||||||
|
symbol: Optional[str] = Query(None, description="币种筛选,如 BTC")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取 Hyperliquid 持仓信息
|
||||||
|
|
||||||
|
返回所有净持仓(Position Netting 模式)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
service = get_hyperliquid_service()
|
||||||
|
|
||||||
|
if service is None:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"enabled": False,
|
||||||
|
"message": "Hyperliquid 服务未启用",
|
||||||
|
"positions": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取持仓
|
||||||
|
all_positions = service.get_open_positions()
|
||||||
|
|
||||||
|
# 筛选指定币种
|
||||||
|
if symbol:
|
||||||
|
symbol = symbol.replace("USDT", "") # 兼容输入
|
||||||
|
all_positions = [p for p in all_positions if p["coin"] == symbol]
|
||||||
|
|
||||||
|
# 获取每个持仓的止盈止损价格
|
||||||
|
positions_data = []
|
||||||
|
for pos in all_positions:
|
||||||
|
coin = pos["coin"]
|
||||||
|
size = pos["size"]
|
||||||
|
|
||||||
|
# 获取止盈止损
|
||||||
|
tp_sl = service.get_tp_sl_prices(coin)
|
||||||
|
|
||||||
|
positions_data.append({
|
||||||
|
"symbol": f"{coin}USDT",
|
||||||
|
"side": "long" if size > 0 else "short",
|
||||||
|
"size": abs(size),
|
||||||
|
"entry_price": pos["entry_price"],
|
||||||
|
"unrealized_pnl": pos["unrealized_pnl"],
|
||||||
|
"leverage": pos.get("leverage", {}).get("value", "N/A"),
|
||||||
|
"liquidation_price": pos.get("liquidation_price"),
|
||||||
|
"take_profit": tp_sl.get("take_profit"),
|
||||||
|
"stop_loss": tp_sl.get("stop_loss"),
|
||||||
|
"position": pos.get("position") # 原始数据
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"enabled": True,
|
||||||
|
"count": len(positions_data),
|
||||||
|
"positions": positions_data
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取 Hyperliquid 持仓失败: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/orders")
|
||||||
|
async def get_orders(
|
||||||
|
symbol: Optional[str] = Query(None, description="币种筛选,如 BTC")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取 Hyperliquid 挂单信息
|
||||||
|
|
||||||
|
包括:
|
||||||
|
- 限价单(未成交的开仓/平仓单)
|
||||||
|
- 止盈止损单(reduce_only=True)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
service = get_hyperliquid_service()
|
||||||
|
|
||||||
|
if service is None:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"enabled": False,
|
||||||
|
"message": "Hyperliquid 服务未启用",
|
||||||
|
"orders": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取所有挂单
|
||||||
|
all_orders = service.get_open_orders()
|
||||||
|
|
||||||
|
# 筛选指定币种
|
||||||
|
if symbol:
|
||||||
|
symbol = symbol.replace("USDT", "") # 兼容输入
|
||||||
|
all_orders = [o for o in all_orders if o["symbol"] == symbol]
|
||||||
|
|
||||||
|
# 分类挂单
|
||||||
|
entry_orders = []
|
||||||
|
tp_sl_orders = []
|
||||||
|
|
||||||
|
for order in all_orders:
|
||||||
|
order_data = {
|
||||||
|
"order_id": order.get("order_id"),
|
||||||
|
"symbol": f"{order['symbol']}USDT",
|
||||||
|
"side": order.get("side"),
|
||||||
|
"size": order.get("size"),
|
||||||
|
"price": order.get("price"),
|
||||||
|
"is_reduce_only": order.get("is_reduce_only", False),
|
||||||
|
"order_type": order.get("order_type", {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.get("is_reduce_only"):
|
||||||
|
tp_sl_orders.append(order_data)
|
||||||
|
else:
|
||||||
|
entry_orders.append(order_data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"enabled": True,
|
||||||
|
"counts": {
|
||||||
|
"entry_orders": len(entry_orders),
|
||||||
|
"tp_sl_orders": len(tp_sl_orders),
|
||||||
|
"total": len(all_orders)
|
||||||
|
},
|
||||||
|
"entry_orders": entry_orders,
|
||||||
|
"tp_sl_orders": tp_sl_orders
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取 Hyperliquid 挂单失败: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/summary")
|
||||||
|
async def get_summary():
|
||||||
|
"""
|
||||||
|
获取 Hyperliquid 交易摘要
|
||||||
|
|
||||||
|
一次性获取账户、持仓、订单的概要信息
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
service = get_hyperliquid_service()
|
||||||
|
|
||||||
|
if service is None:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"enabled": False,
|
||||||
|
"message": "Hyperliquid 服务未启用"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取账户状态
|
||||||
|
account_state = service.get_account_state()
|
||||||
|
|
||||||
|
# 获取持仓
|
||||||
|
positions = service.get_open_positions()
|
||||||
|
total_position_value = sum(abs(p["size"]) * p["entry_price"] for p in positions)
|
||||||
|
|
||||||
|
# 获取挂单
|
||||||
|
orders = service.get_open_orders()
|
||||||
|
|
||||||
|
# 计算杠杆
|
||||||
|
if account_state["account_value"] > 0:
|
||||||
|
current_leverage = total_position_value / account_state["account_value"]
|
||||||
|
else:
|
||||||
|
current_leverage = 0
|
||||||
|
|
||||||
|
# 计算回撤
|
||||||
|
if service.initial_balance and service.initial_balance > 0:
|
||||||
|
drawdown = (service.initial_balance - account_state["account_value"]) / service.initial_balance
|
||||||
|
else:
|
||||||
|
drawdown = 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"enabled": True,
|
||||||
|
"data": {
|
||||||
|
"account": {
|
||||||
|
"account_value": account_state["account_value"],
|
||||||
|
"available_balance": account_state["available_balance"],
|
||||||
|
"total_margin_used": account_state["total_margin_used"]
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"count": len(positions),
|
||||||
|
"total_value": total_position_value
|
||||||
|
},
|
||||||
|
"orders": {
|
||||||
|
"count": len(orders),
|
||||||
|
"entry_orders": len([o for o in orders if not o.get("is_reduce_only", False)]),
|
||||||
|
"tp_sl_orders": len([o for o in orders if o.get("is_reduce_only", False)])
|
||||||
|
},
|
||||||
|
"risk": {
|
||||||
|
"current_leverage": current_leverage,
|
||||||
|
"max_leverage": service.max_total_leverage,
|
||||||
|
"leverage_utilization": (current_leverage / service.max_total_leverage * 100) if service.max_total_leverage > 0 else 0,
|
||||||
|
"drawdown": drawdown * 100,
|
||||||
|
"circuit_breaker_threshold": service.circuit_breaker_drawdown * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取 Hyperliquid 摘要失败: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/orders/cancel")
|
||||||
|
async def cancel_orders(
|
||||||
|
symbol: str = Query(..., description="币种,如 BTC")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
取消指定币种的所有挂单
|
||||||
|
|
||||||
|
包括开仓单和止盈止损单
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
service = get_hyperliquid_service()
|
||||||
|
|
||||||
|
if service is None:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Hyperliquid 服务未启用"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 取消该币种的所有订单
|
||||||
|
result = service.cancel_all_orders(symbol.replace("USDT", ""))
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"已取消 {symbol} 的所有挂单"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("error", "取消失败")
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"取消 Hyperliquid 挂单失败: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/positions/close")
|
||||||
|
async def close_position(
|
||||||
|
symbol: str = Query(..., description="币种,如 BTC")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
平掉指定币种的所有持仓(市价平仓)
|
||||||
|
|
||||||
|
⚠️ 警告:此操作会立即以市价平仓,请谨慎使用
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
service = get_hyperliquid_service()
|
||||||
|
|
||||||
|
if service is None:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Hyperliquid 服务未启用"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取持仓
|
||||||
|
position = service.get_position_for_symbol(symbol.replace("USDT", ""))
|
||||||
|
|
||||||
|
if not position:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"未找到 {symbol} 的持仓"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 取消所有挂单(包括止盈止损)
|
||||||
|
service.cancel_tp_sl_orders(symbol.replace("USDT", ""))
|
||||||
|
|
||||||
|
# 市价平仓
|
||||||
|
size = abs(position["size"])
|
||||||
|
is_long = position["size"] > 0
|
||||||
|
|
||||||
|
result = service.place_market_order(
|
||||||
|
symbol=symbol.replace("USDT", ""),
|
||||||
|
is_buy=not is_long,
|
||||||
|
size=size,
|
||||||
|
reduce_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"已平仓 {symbol} {size} @ 市价"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("error", "平仓失败")
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Hyperliquid 平仓失败: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@ -9,7 +9,7 @@ from fastapi.responses import FileResponse
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals, system, real_trading, news, astock
|
from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals, system, real_trading, news, astock, hyperliquid
|
||||||
from app.utils.error_handler import setup_global_exception_handler, init_error_notifier
|
from app.utils.error_handler import setup_global_exception_handler, init_error_notifier
|
||||||
from app.utils.system_status import get_system_monitor
|
from app.utils.system_status import get_system_monitor
|
||||||
import os
|
import os
|
||||||
@ -673,6 +673,7 @@ app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"])
|
|||||||
app.include_router(llm.router, tags=["LLM模型"])
|
app.include_router(llm.router, tags=["LLM模型"])
|
||||||
app.include_router(paper_trading.router, tags=["交易"])
|
app.include_router(paper_trading.router, tags=["交易"])
|
||||||
app.include_router(real_trading.router, tags=["实盘交易"])
|
app.include_router(real_trading.router, tags=["实盘交易"])
|
||||||
|
app.include_router(hyperliquid.router, tags=["Hyperliquid"])
|
||||||
app.include_router(stocks.router, prefix="/api/stocks", tags=["美股分析"])
|
app.include_router(stocks.router, prefix="/api/stocks", tags=["美股分析"])
|
||||||
app.include_router(astock.router, prefix="/api/astock", tags=["A股分析"])
|
app.include_router(astock.router, prefix="/api/astock", tags=["A股分析"])
|
||||||
app.include_router(signals.router, tags=["信号管理"])
|
app.include_router(signals.router, tags=["信号管理"])
|
||||||
@ -737,6 +738,14 @@ async def status_page():
|
|||||||
return FileResponse(page_path)
|
return FileResponse(page_path)
|
||||||
return {"message": "页面不存在"}
|
return {"message": "页面不存在"}
|
||||||
|
|
||||||
|
@app.get("/hyperliquid")
|
||||||
|
async def hyperliquid_page():
|
||||||
|
"""Hyperliquid 交易监控页面"""
|
||||||
|
page_path = os.path.join(frontend_path, "hyperliquid.html")
|
||||||
|
if os.path.exists(page_path):
|
||||||
|
return FileResponse(page_path)
|
||||||
|
return {"message": "页面不存在"}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|||||||
716
frontend/hyperliquid.html
Normal file
716
frontend/hyperliquid.html
Normal file
@ -0,0 +1,716 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hyperliquid 交易监控 | AI Agent</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-enabled {
|
||||||
|
background: #238636;
|
||||||
|
color: #3fb950;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-disabled {
|
||||||
|
background: #8c959f;
|
||||||
|
color: #6e7681;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #21262d;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
color: #c9d1d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover {
|
||||||
|
background: #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn.loading {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 账户信息卡片 */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.positive {
|
||||||
|
color: #3fb950;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.negative {
|
||||||
|
color: #f85149;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.warning {
|
||||||
|
color: #d29922;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区域 */
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
background: #0d1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表样式 */
|
||||||
|
.list-item {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #21262d;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #c9d1d9;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-long {
|
||||||
|
background: #238636;
|
||||||
|
color: #3fb950;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-short {
|
||||||
|
background: #8b3c3c;
|
||||||
|
color: #f85149;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail span {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pnl {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pnl.positive {
|
||||||
|
color: #3fb950;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pnl.negative {
|
||||||
|
color: #f85149;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6e7681;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮 */
|
||||||
|
.action-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #21262d;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
color: #c9d1d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.danger {
|
||||||
|
border-color: #f85149;
|
||||||
|
color: #f85149;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.danger:hover {
|
||||||
|
background: #f85149;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
min-width: 80px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-details {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid #30363d;
|
||||||
|
border-top-color: #58a6ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单类型标签 */
|
||||||
|
.order-type-badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-entry {
|
||||||
|
background: #1f6feb;
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-tp {
|
||||||
|
background: #238636;
|
||||||
|
color: #3fb950;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-sl {
|
||||||
|
background: #8b3c3c;
|
||||||
|
color: #f85149;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 风险指标 */
|
||||||
|
.risk-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
background: #21262d;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #58a6ff;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-fill.warning {
|
||||||
|
background: #d29922;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-fill.danger {
|
||||||
|
background: #f85149;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="container">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">🔥 Hyperliquid 交易监控</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 12px;">
|
||||||
|
<span class="status-badge" :class="enabled ? 'status-enabled' : 'status-disabled'">
|
||||||
|
{{ enabled ? '已启用' : '未启用' }}
|
||||||
|
</span>
|
||||||
|
<button class="refresh-btn" @click="refreshData" :class="{ loading: loading }">
|
||||||
|
<span v-if="loading" class="spinner"></span>
|
||||||
|
{{ loading ? '刷新中...' : '刷新' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未启用提示 -->
|
||||||
|
<div v-if="!enabled" class="empty-state">
|
||||||
|
<div class="empty-icon">🔒</div>
|
||||||
|
<p>Hyperliquid 实盘交易未启用</p>
|
||||||
|
<p style="font-size: 13px; margin-top: 8px;">请在 .env 文件中设置 hyperliquid_trading_enabled=true</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已启用内容 -->
|
||||||
|
<div v-else>
|
||||||
|
<!-- 账户信息 -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">账户价值</div>
|
||||||
|
<div class="stat-value">${{ formatNumber(account.account_value) }}</div>
|
||||||
|
<div class="stat-sub">可用: ${{ formatNumber(account.available_balance) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">总持仓价值</div>
|
||||||
|
<div class="stat-value">${{ formatNumber(account.total_position_value) }}</div>
|
||||||
|
<div class="stat-sub">{{ positions.length }} 个持仓</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">总杠杆率</div>
|
||||||
|
<div class="stat-value" :class="getLeverageClass(account.current_leverage)">
|
||||||
|
{{ account.current_leverage?.toFixed(1) || '0.0' }}x
|
||||||
|
</div>
|
||||||
|
<div class="stat-sub">
|
||||||
|
上限: {{ account.max_leverage }}x
|
||||||
|
<span v-if="leverageUtilization > 80" style="color: #d29922; margin-left: 8px;">
|
||||||
|
⚠️
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">回撤</div>
|
||||||
|
<div class="stat-value" :class="getDrawdownClass(account.drawdown)">
|
||||||
|
{{ account.drawdown?.toFixed(1) || '0.0' }}%
|
||||||
|
</div>
|
||||||
|
<div class="stat-sub">熔断线: {{ account.circuit_breaker_threshold?.toFixed(0) }}%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">已用保证金</div>
|
||||||
|
<div class="stat-value">${{ formatNumber(account.total_margin_used) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">挂单数量</div>
|
||||||
|
<div class="stat-value">{{ orders.entry_orders + orders.tp_sl_orders }}</div>
|
||||||
|
<div class="stat-sub">
|
||||||
|
开仓: {{ orders.entry_orders }} |
|
||||||
|
TP/SL: {{ orders.tp_sl_orders }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 持仓和挂单 -->
|
||||||
|
<div class="content-grid">
|
||||||
|
<!-- 持仓列表 -->
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="panel-title">📈 持仓 ({{ positions.length }})</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div v-if="positions.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">📊</div>
|
||||||
|
<p>暂无持仓</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="pos in positions" :key="pos.symbol" class="list-item">
|
||||||
|
<div class="item-left">
|
||||||
|
<div>
|
||||||
|
<div class="symbol">{{ pos.symbol }}</div>
|
||||||
|
<span class="side-badge" :class="pos.side === 'long' ? 'side-long' : 'side-short'">
|
||||||
|
{{ pos.side === 'long' ? '做多' : '做空' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-details">
|
||||||
|
<div class="item-detail">
|
||||||
|
数量: <span>{{ pos.size?.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-detail">
|
||||||
|
入场价: <span>${{ formatNumber(pos.entry_price) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-detail">
|
||||||
|
杠杆: <span>{{ pos.leverage }}x</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-right">
|
||||||
|
<div class="pnl" :class="getPnLClass(pos.unrealized_pnl)">
|
||||||
|
{{ pos.unrealized_pnl >= 0 ? '+' : '' }}${{ formatNumber(pos.unrealized_pnl) }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 11px; color: #8b949e; margin-top: 4px;">
|
||||||
|
TP: ${{ formatNumber(pos.take_profit) }} | SL: ${{ formatNumber(pos.stop_loss) }}
|
||||||
|
</div>
|
||||||
|
<button class="action-btn danger" @click="closePosition(pos.symbol)" style="margin-top: 8px;">
|
||||||
|
平仓
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 挂单列表 -->
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="panel-title">📋 挂单 ({{ orders.entry_orders + orders.tp_sl_orders }})</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div v-if="orders.entry_orders + orders.tp_sl_orders === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<p>暂无挂单</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<!-- 入场单 -->
|
||||||
|
<div v-if="entryOrdersList.length > 0">
|
||||||
|
<div style="padding: 12px 20px; font-size: 12px; color: #8b949e; background: #0d1117;">
|
||||||
|
入场单 ({{ entryOrdersList.length }})
|
||||||
|
</div>
|
||||||
|
<div v-for="order in entryOrdersList" :key="order.order_id" class="list-item">
|
||||||
|
<div class="item-left">
|
||||||
|
<div class="symbol">{{ order.symbol }}</div>
|
||||||
|
<div class="item-details">
|
||||||
|
<div class="item-detail">
|
||||||
|
{{ order.side === 'B' ? '买入' : '卖出' }}
|
||||||
|
</div>
|
||||||
|
<div class="item-detail">
|
||||||
|
@ ${{ formatNumber(order.price) }}
|
||||||
|
</div>
|
||||||
|
<div class="item-detail">
|
||||||
|
{{ order.size }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-right">
|
||||||
|
<button class="action-btn danger" @click="cancelOrder(order.symbol)">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 止盈止损单 -->
|
||||||
|
<div v-if="tpSlOrdersList.length > 0">
|
||||||
|
<div style="padding: 12px 20px; font-size: 12px; color: #8b949e; background: #0d1117; border-top: 1px solid #30363d;">
|
||||||
|
止盈止损 ({{ tpSlOrdersList.length }})
|
||||||
|
</div>
|
||||||
|
<div v-for="order in tpSlOrdersList" :key="order.order_id" class="list-item">
|
||||||
|
<div class="item-left">
|
||||||
|
<div class="symbol">{{ order.symbol }}</div>
|
||||||
|
<div class="item-details">
|
||||||
|
<div class="item-detail">
|
||||||
|
<span class="order-type-badge order-tp">TP</span>
|
||||||
|
<span class="order-type-badge order-sl">SL</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-detail">
|
||||||
|
@ ${{ formatNumber(order.price) }}
|
||||||
|
</div>
|
||||||
|
<div class="item-detail">
|
||||||
|
{{ order.size }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-right">
|
||||||
|
<span style="font-size: 11px; color: #8b949e;">自动执行</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const { createApp } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
enabled: false,
|
||||||
|
account: {
|
||||||
|
account_value: 0,
|
||||||
|
available_balance: 0,
|
||||||
|
total_margin_used: 0,
|
||||||
|
total_position_value: 0,
|
||||||
|
current_leverage: 0,
|
||||||
|
max_leverage: 10,
|
||||||
|
drawdown: 0,
|
||||||
|
circuit_breaker_threshold: 10
|
||||||
|
},
|
||||||
|
positions: [],
|
||||||
|
orders: {
|
||||||
|
entry_orders: 0,
|
||||||
|
tp_sl_orders: 0,
|
||||||
|
all: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
leverageUtilization() {
|
||||||
|
if (!this.account.max_leverage) return 0;
|
||||||
|
return (this.account.current_leverage / this.account.max_leverage) * 100;
|
||||||
|
},
|
||||||
|
entryOrdersList() {
|
||||||
|
return this.orders.all.filter(o => !o.is_reduce_only);
|
||||||
|
},
|
||||||
|
tpSlOrdersList() {
|
||||||
|
return this.orders.all.filter(o => o.is_reduce_only);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async refreshData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
// 并发获取所有数据
|
||||||
|
const [accountRes, positionsRes, ordersRes] = await Promise.all([
|
||||||
|
fetch('/api/hyperliquid/account').then(r => r.json()),
|
||||||
|
fetch('/api/hyperliquid/positions').then(r => r.json()),
|
||||||
|
fetch('/api/hyperliquid/orders').then(r => r.json())
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 处理账户数据
|
||||||
|
if (accountRes.success && accountRes.data) {
|
||||||
|
this.enabled = accountRes.enabled;
|
||||||
|
this.account = accountRes.data;
|
||||||
|
} else {
|
||||||
|
this.enabled = accountRes.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理持仓数据
|
||||||
|
if (positionsRes.success && positionsRes.positions) {
|
||||||
|
this.positions = positionsRes.positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理订单数据
|
||||||
|
if (ordersRes.success) {
|
||||||
|
this.orders = {
|
||||||
|
entry_orders: ordersRes.counts.entry_orders,
|
||||||
|
tp_sl_orders: ordersRes.counts.tp_sl_orders,
|
||||||
|
all: [...ordersRes.entry_orders, ...ordersRes.tp_sl_orders]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatNumber(num) {
|
||||||
|
if (num === null || num === undefined) return '0.00';
|
||||||
|
return parseFloat(num).toLocaleString('en-US', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getLeverageClass(leverage) {
|
||||||
|
if (leverage >= 8) return 'warning';
|
||||||
|
if (leverage >= 9) return 'negative';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
getDrawdownClass(drawdown) {
|
||||||
|
if (drawdown >= 8) return 'warning';
|
||||||
|
if (drawdown >= 9) return 'negative';
|
||||||
|
if (drawdown < 0) return 'positive';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
getPnLClass(pnl) {
|
||||||
|
if (pnl > 0) return 'positive';
|
||||||
|
if (pnl < 0) return 'negative';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
async closePosition(symbol) {
|
||||||
|
if (!confirm(`确定要平掉 ${symbol} 的持仓吗?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/hyperliquid/positions/close?symbol=${symbol}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('平仓成功');
|
||||||
|
await this.refreshData();
|
||||||
|
} else {
|
||||||
|
alert('平仓失败: ' + result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('平仓失败: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async cancelOrder(symbol) {
|
||||||
|
if (!confirm(`确定要取消 ${symbol} 的所有挂单吗?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/hyperliquid/orders/cancel?symbol=${symbol}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('取消成功');
|
||||||
|
await this.refreshData();
|
||||||
|
} else {
|
||||||
|
alert('取消失败: ' + result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('取消失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.refreshData();
|
||||||
|
// 自动刷新(每30秒)
|
||||||
|
setInterval(() => {
|
||||||
|
this.refreshData();
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
}).mount('#app');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user