+2
-1
@@ -42,7 +42,8 @@ __pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
dist/
|
||||
# Only ignore root-level dist/, not frontend/dist/
|
||||
/dist/
|
||||
build/
|
||||
*.egg
|
||||
.eggs/
|
||||
|
||||
@@ -5,10 +5,11 @@ Allows users to place market or limit orders directly from AI analysis
|
||||
or indicator analysis pages, without creating a strategy first.
|
||||
|
||||
Endpoints:
|
||||
POST /api/quick-trade/place-order — Place a quick order
|
||||
GET /api/quick-trade/balance — Get available balance
|
||||
GET /api/quick-trade/position — Get current position for symbol
|
||||
GET /api/quick-trade/history — Get quick trade history
|
||||
POST /api/quick-trade/place-order — Place a quick order
|
||||
POST /api/quick-trade/close-position — Close an existing position
|
||||
GET /api/quick-trade/balance — Get available balance
|
||||
GET /api/quick-trade/position — Get current position for symbol
|
||||
GET /api/quick-trade/history — Get quick trade history
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -32,6 +33,101 @@ quick_trade_bp = Blueprint('quick_trade', __name__)
|
||||
|
||||
# ────────── helpers ──────────
|
||||
|
||||
def _convert_usdt_to_base_qty(client, symbol: str, usdt_amount: float, market_type: str, limit_price: float = 0.0) -> float:
|
||||
"""
|
||||
Convert USDT amount to base asset quantity for all exchanges.
|
||||
|
||||
This is a unified function that works for all exchanges.
|
||||
For spot: converts USDT -> base qty (e.g., 100 USDT -> 0.033 ETH)
|
||||
For swap: converts USDT -> base qty (e.g., 100 USDT -> 0.033 ETH), which will then be converted to contracts
|
||||
|
||||
Args:
|
||||
client: Exchange client instance
|
||||
symbol: Trading pair (e.g., "ETH/USDT")
|
||||
usdt_amount: USDT amount to convert
|
||||
market_type: "spot" or "swap"
|
||||
limit_price: For limit orders, use this price if provided (optional)
|
||||
|
||||
Returns:
|
||||
Base asset quantity
|
||||
"""
|
||||
if usdt_amount <= 0:
|
||||
return usdt_amount
|
||||
|
||||
try:
|
||||
# Try to get current price from exchange
|
||||
current_price = 0.0
|
||||
|
||||
# For limit orders, use the provided price
|
||||
if limit_price > 0:
|
||||
current_price = limit_price
|
||||
logger.info(f"Using limit price {limit_price} for USDT conversion")
|
||||
else:
|
||||
# Try to get current market price from exchange
|
||||
# OKX
|
||||
from app.services.live_trading.okx import OkxClient
|
||||
if isinstance(client, OkxClient):
|
||||
try:
|
||||
from app.services.live_trading.symbols import to_okx_spot_inst_id, to_okx_swap_inst_id
|
||||
inst_id = to_okx_spot_inst_id(symbol) if market_type == "spot" else to_okx_swap_inst_id(symbol)
|
||||
logger.debug(f"OKX: Getting ticker for inst_id={inst_id}, symbol={symbol}, market_type={market_type}")
|
||||
ticker = client.get_ticker(inst_id=inst_id)
|
||||
if ticker:
|
||||
current_price = float(ticker.get("last") or ticker.get("lastPx") or 0)
|
||||
logger.debug(f"OKX: Got price {current_price} from ticker")
|
||||
else:
|
||||
logger.warning(f"OKX: get_ticker returned empty result for inst_id={inst_id}")
|
||||
except AttributeError as e:
|
||||
logger.error(f"OKX: get_ticker method not found: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"OKX: Failed to get ticker: {e}")
|
||||
raise
|
||||
|
||||
# Binance - try to get price from public API
|
||||
from app.services.live_trading.binance import BinanceFuturesClient
|
||||
from app.services.live_trading.binance_spot import BinanceSpotClient
|
||||
if isinstance(client, (BinanceFuturesClient, BinanceSpotClient)):
|
||||
try:
|
||||
# Binance public ticker endpoint
|
||||
base_url = getattr(client, "base_url", "")
|
||||
if "binance" in base_url.lower():
|
||||
import requests
|
||||
if isinstance(client, BinanceFuturesClient):
|
||||
ticker_url = f"{base_url}/fapi/v1/ticker/price"
|
||||
else:
|
||||
ticker_url = f"{base_url}/api/v3/ticker/price"
|
||||
from app.services.live_trading.symbols import to_binance_futures_symbol
|
||||
# Binance spot and futures use the same symbol format
|
||||
sym = to_binance_futures_symbol(symbol)
|
||||
resp = requests.get(ticker_url, params={"symbol": sym}, timeout=5)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if isinstance(data, dict):
|
||||
current_price = float(data.get("price") or 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Other exchanges - can be added as needed
|
||||
# For exchanges without price API, we'll use a fallback
|
||||
|
||||
if current_price > 0:
|
||||
base_qty = usdt_amount / current_price
|
||||
logger.info(f"Converted USDT amount {usdt_amount} to base qty {base_qty:.8f} using price {current_price} for {symbol}")
|
||||
return base_qty
|
||||
else:
|
||||
# Can't get price - this is critical for quick trade
|
||||
# Quick trade always expects USDT input, so we must convert
|
||||
logger.error(f"CRITICAL: Could not get price for {symbol} on {type(client).__name__} to convert USDT amount {usdt_amount}")
|
||||
logger.error(f"This will cause order to fail. Please check exchange API connectivity or symbol format.")
|
||||
# Still return original amount as fallback, but log error
|
||||
return usdt_amount
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to convert USDT amount to base qty: {e}, using original amount")
|
||||
return usdt_amount
|
||||
|
||||
|
||||
def _safe_json(v, default=None):
|
||||
if v is None:
|
||||
return default
|
||||
@@ -138,10 +234,12 @@ def place_order():
|
||||
symbol (str) — e.g. "BTC/USDT"
|
||||
side (str) — "buy" or "sell"
|
||||
order_type (str) — "market" or "limit" (default: market)
|
||||
amount (float) — order size (USDT quote amount for market buy, or base qty)
|
||||
amount (float) — USDT amount (always in USDT, will be converted to base qty)
|
||||
price (float) — limit price (required for limit orders)
|
||||
leverage (int) — leverage multiplier (default: 1)
|
||||
market_type (str) — "swap" / "spot" (default: swap)
|
||||
- leverage = 1: spot market
|
||||
- leverage > 1: swap (perpetual futures) market
|
||||
market_type (str) — "swap" / "spot" (optional, auto-determined by leverage if not provided)
|
||||
tp_price (float) — take-profit price (optional, for record only)
|
||||
sl_price (float) — stop-loss price (optional, for record only)
|
||||
source (str) — "ai_radar" / "ai_analysis" / "indicator" / "manual"
|
||||
@@ -154,10 +252,10 @@ def place_order():
|
||||
symbol = str(body.get("symbol") or "").strip()
|
||||
side = str(body.get("side") or "").strip().lower()
|
||||
order_type = str(body.get("order_type") or "market").strip().lower()
|
||||
amount = float(body.get("amount") or 0)
|
||||
usdt_amount = float(body.get("amount") or 0) # Always USDT amount
|
||||
price = float(body.get("price") or 0)
|
||||
leverage = int(body.get("leverage") or 1)
|
||||
market_type = str(body.get("market_type") or "swap").strip().lower()
|
||||
market_type = str(body.get("market_type") or "").strip().lower()
|
||||
tp_price = float(body.get("tp_price") or 0)
|
||||
sl_price = float(body.get("sl_price") or 0)
|
||||
source = str(body.get("source") or "manual").strip()
|
||||
@@ -169,13 +267,22 @@ def place_order():
|
||||
return jsonify({"code": 0, "msg": "Missing symbol"}), 400
|
||||
if side not in ("buy", "sell"):
|
||||
return jsonify({"code": 0, "msg": "side must be 'buy' or 'sell'"}), 400
|
||||
if amount <= 0:
|
||||
if usdt_amount <= 0:
|
||||
return jsonify({"code": 0, "msg": "amount must be > 0"}), 400
|
||||
if order_type == "limit" and price <= 0:
|
||||
return jsonify({"code": 0, "msg": "price required for limit orders"}), 400
|
||||
|
||||
# ---- Auto-determine market_type from leverage ----
|
||||
# leverage = 1 -> spot, leverage > 1 -> swap
|
||||
if not market_type:
|
||||
market_type = "spot" if leverage == 1 else "swap"
|
||||
if market_type in ("futures", "future", "perp", "perpetual"):
|
||||
market_type = "swap"
|
||||
# Override: if leverage > 1, force swap; if leverage = 1, force spot
|
||||
if leverage > 1:
|
||||
market_type = "swap"
|
||||
elif leverage == 1:
|
||||
market_type = "spot"
|
||||
|
||||
# ---- build exchange client ----
|
||||
exchange_config = _build_exchange_config(credential_id, user_id, {
|
||||
@@ -187,31 +294,82 @@ def place_order():
|
||||
|
||||
client = _create_client(exchange_config, market_type=market_type)
|
||||
|
||||
# ---- Convert USDT amount to base asset quantity ----
|
||||
# Quick trade always accepts USDT amount, convert to base qty for all exchanges
|
||||
# For limit orders, use the provided price; for market orders, fetch current price
|
||||
limit_price_for_conversion = price if order_type == "limit" and price > 0 else 0.0
|
||||
base_qty = _convert_usdt_to_base_qty(client, symbol, usdt_amount, market_type, limit_price_for_conversion)
|
||||
|
||||
# Validate conversion: if base_qty equals usdt_amount, conversion likely failed
|
||||
# For swap markets, base_qty should be much smaller than usdt_amount (e.g., 100 USDT -> 0.033 ETH)
|
||||
if market_type != "spot" and base_qty == usdt_amount and usdt_amount >= 1:
|
||||
logger.error(f"USDT conversion may have failed: base_qty ({base_qty}) equals usdt_amount ({usdt_amount})")
|
||||
logger.error(f"This suggests the price fetch failed. Order may fail due to insufficient margin.")
|
||||
|
||||
# ---- set leverage (futures only) ----
|
||||
if market_type != "spot" and leverage > 1:
|
||||
try:
|
||||
if hasattr(client, "set_leverage"):
|
||||
client.set_leverage(symbol=symbol, leverage=leverage)
|
||||
elif hasattr(client, "set_leverage") and callable(getattr(client, "set_leverage", None)):
|
||||
client.set_leverage(symbol=symbol, lever=leverage)
|
||||
from app.services.live_trading.okx import OkxClient
|
||||
from app.services.live_trading.gate import GateUsdtFuturesClient
|
||||
|
||||
# OKX requires inst_id instead of symbol
|
||||
if isinstance(client, OkxClient):
|
||||
from app.services.live_trading.symbols import to_okx_swap_inst_id
|
||||
inst_id = to_okx_swap_inst_id(symbol)
|
||||
client.set_leverage(inst_id=inst_id, lever=leverage)
|
||||
# Gate requires contract (currency_pair) instead of symbol
|
||||
elif isinstance(client, GateUsdtFuturesClient):
|
||||
from app.services.live_trading.symbols import to_gate_currency_pair
|
||||
contract = to_gate_currency_pair(symbol)
|
||||
client.set_leverage(contract=contract, leverage=leverage)
|
||||
# Most other exchanges use symbol
|
||||
else:
|
||||
# Try common parameter names
|
||||
try:
|
||||
client.set_leverage(symbol=symbol, leverage=leverage)
|
||||
except TypeError:
|
||||
try:
|
||||
client.set_leverage(symbol=symbol, lever=leverage)
|
||||
except TypeError:
|
||||
pass
|
||||
except Exception as le:
|
||||
logger.warning(f"set_leverage failed (non-fatal): {le}")
|
||||
|
||||
# ---- place order ----
|
||||
client_order_id = f"qt_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||
# Generate client_order_id: OKX clOrdId requirements: 1-32 chars, alphanumeric, underscore, hyphen only
|
||||
timestamp_suffix = str(int(time.time()))[-6:] # Last 6 digits of timestamp
|
||||
uuid_suffix = uuid.uuid4().hex[:8] # 8 hex chars
|
||||
client_order_id = f"qt{timestamp_suffix}{uuid_suffix}" # Total: 2 + 6 + 8 = 16 chars
|
||||
|
||||
result = None
|
||||
if order_type == "market":
|
||||
result = client.place_market_order(
|
||||
# Use execution.py's place_order_from_signal for market orders to ensure consistency
|
||||
# Convert side to signal_type: buy -> open_long, sell -> open_short (for swap) or close_long (for spot)
|
||||
from app.services.live_trading.execution import place_order_from_signal
|
||||
|
||||
if market_type == "spot":
|
||||
# Spot: buy = open_long, sell = close_long (assuming we're closing a position)
|
||||
signal_type = "open_long" if side == "buy" else "close_long"
|
||||
else:
|
||||
# Swap: buy = open_long, sell = open_short
|
||||
signal_type = "open_long" if side == "buy" else "open_short"
|
||||
|
||||
result = place_order_from_signal(
|
||||
client=client,
|
||||
signal_type=signal_type,
|
||||
symbol=symbol,
|
||||
side=side.upper() if "binance" in exchange_id else side,
|
||||
**_market_order_kwargs(client, symbol, amount, side, market_type, client_order_id),
|
||||
amount=base_qty, # Use converted base qty
|
||||
market_type=market_type,
|
||||
exchange_config=exchange_config,
|
||||
client_order_id=client_order_id,
|
||||
)
|
||||
else:
|
||||
# Limit orders: use direct client call (execution.py doesn't handle limit orders)
|
||||
result = client.place_limit_order(
|
||||
symbol=symbol,
|
||||
side=side.upper() if "binance" in exchange_id else side,
|
||||
**_limit_order_kwargs(client, symbol, amount, price, side, market_type, client_order_id),
|
||||
**_limit_order_kwargs(client, symbol, base_qty, price, side, market_type, client_order_id),
|
||||
)
|
||||
|
||||
# ---- extract result ----
|
||||
@@ -221,6 +379,7 @@ def place_order():
|
||||
raw = getattr(result, "raw", {}) or {}
|
||||
|
||||
# ---- record trade ----
|
||||
# Record original USDT amount, not converted base qty
|
||||
trade_id = _record_quick_trade(
|
||||
user_id=user_id,
|
||||
credential_id=credential_id,
|
||||
@@ -228,7 +387,7 @@ def place_order():
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
order_type=order_type,
|
||||
amount=amount,
|
||||
amount=usdt_amount, # Record original USDT amount
|
||||
price=price if order_type == "limit" else avg_fill,
|
||||
leverage=leverage,
|
||||
market_type=market_type,
|
||||
@@ -268,7 +427,7 @@ def place_order():
|
||||
symbol=str(body.get("symbol") or ""),
|
||||
side=str(body.get("side") or ""),
|
||||
order_type=str(body.get("order_type") or "market"),
|
||||
amount=float(body.get("amount") or 0),
|
||||
amount=float(body.get("amount") or 0), # Original USDT amount
|
||||
price=0,
|
||||
leverage=int(body.get("leverage") or 1),
|
||||
market_type=str(body.get("market_type") or "swap"),
|
||||
@@ -299,7 +458,14 @@ def _market_order_kwargs(client, symbol, amount, side, market_type, client_order
|
||||
if isinstance(client, (BinanceFuturesClient, BinanceSpotClient)):
|
||||
return {"quantity": amount, "client_order_id": client_order_id}
|
||||
if isinstance(client, OkxClient):
|
||||
return {"size": amount, "client_order_id": client_order_id}
|
||||
kwargs = {"market_type": market_type, "size": amount, "client_order_id": client_order_id}
|
||||
# For swap market, OKX requires pos_side. Infer from side:
|
||||
# buy -> long, sell -> short
|
||||
# The _resolve_pos_side method will handle net_mode vs long_short_mode
|
||||
if market_type and market_type.strip().lower() != "spot":
|
||||
pos_side = "long" if side.lower() == "buy" else "short"
|
||||
kwargs["pos_side"] = pos_side
|
||||
return kwargs
|
||||
if isinstance(client, BitgetMixClient):
|
||||
return {"size": amount, "client_order_id": client_order_id}
|
||||
if isinstance(client, BybitClient):
|
||||
@@ -312,9 +478,19 @@ def _limit_order_kwargs(client, symbol, amount, price, side, market_type, client
|
||||
"""Build kwargs compatible with any exchange client's place_limit_order."""
|
||||
from app.services.live_trading.binance import BinanceFuturesClient
|
||||
from app.services.live_trading.binance_spot import BinanceSpotClient
|
||||
from app.services.live_trading.okx import OkxClient
|
||||
|
||||
if isinstance(client, (BinanceFuturesClient, BinanceSpotClient)):
|
||||
return {"quantity": amount, "price": price, "client_order_id": client_order_id}
|
||||
if isinstance(client, OkxClient):
|
||||
kwargs = {"market_type": market_type, "size": amount, "price": price, "client_order_id": client_order_id}
|
||||
# For swap market, OKX requires pos_side. Infer from side:
|
||||
# buy -> long, sell -> short
|
||||
# The _resolve_pos_side method will handle net_mode vs long_short_mode
|
||||
if market_type and market_type.strip().lower() != "spot":
|
||||
pos_side = "long" if side.lower() == "buy" else "short"
|
||||
kwargs["pos_side"] = pos_side
|
||||
return kwargs
|
||||
# Generic fallback
|
||||
return {"size": amount, "price": price, "client_order_id": client_order_id}
|
||||
|
||||
@@ -444,7 +620,20 @@ def get_position():
|
||||
|
||||
positions = []
|
||||
try:
|
||||
if hasattr(client, "get_positions"):
|
||||
# OKX requires inst_id instead of symbol
|
||||
from app.services.live_trading.okx import OkxClient
|
||||
if isinstance(client, OkxClient):
|
||||
from app.services.live_trading.symbols import to_okx_swap_inst_id, to_okx_spot_inst_id
|
||||
if market_type == "spot":
|
||||
inst_id = to_okx_spot_inst_id(symbol)
|
||||
inst_type = "SPOT"
|
||||
else:
|
||||
inst_id = to_okx_swap_inst_id(symbol)
|
||||
inst_type = "SWAP"
|
||||
raw = client.get_positions(inst_id=inst_id, inst_type=inst_type)
|
||||
positions = _parse_positions(raw)
|
||||
logger.info(f"OKX positions query: inst_id={inst_id}, inst_type={inst_type}, found {len(positions)} positions")
|
||||
elif hasattr(client, "get_positions"):
|
||||
raw = client.get_positions(symbol=symbol)
|
||||
positions = _parse_positions(raw)
|
||||
elif hasattr(client, "get_position"):
|
||||
@@ -452,7 +641,9 @@ def get_position():
|
||||
positions = _parse_positions(raw)
|
||||
except Exception as pe:
|
||||
logger.warning(f"Position fetch failed: {pe}")
|
||||
logger.warning(traceback.format_exc())
|
||||
|
||||
logger.info(f"Returning {len(positions)} positions for symbol={symbol}, market_type={market_type}")
|
||||
return jsonify({"code": 1, "msg": "success", "data": {"positions": positions}})
|
||||
except Exception as e:
|
||||
logger.error(f"get_position failed: {e}")
|
||||
@@ -478,23 +669,210 @@ def _parse_positions(raw: Any) -> list:
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
size = float(item.get("posAmt") or item.get("pos") or item.get("size") or item.get("contracts") or 0)
|
||||
# For OKX, position size can be in different fields
|
||||
# SWAP: posAmt, pos
|
||||
# SPOT: bal (balance), availBal (available balance)
|
||||
size = float(item.get("posAmt") or item.get("pos") or item.get("size") or item.get("contracts") or
|
||||
item.get("bal") or item.get("availBal") or 0)
|
||||
if abs(size) < 1e-10:
|
||||
continue
|
||||
|
||||
# For spot, side is always "long" (you own the asset)
|
||||
# For swap, determine side from sign of size
|
||||
side = "long"
|
||||
if size < 0:
|
||||
side = "short"
|
||||
elif item.get("posSide"):
|
||||
# OKX may have posSide field: "long" or "short"
|
||||
pos_side = str(item.get("posSide", "")).strip().lower()
|
||||
if pos_side in ("long", "short"):
|
||||
side = pos_side
|
||||
|
||||
result.append({
|
||||
"symbol": item.get("symbol") or item.get("instId") or "",
|
||||
"side": "long" if size > 0 else "short",
|
||||
"side": side,
|
||||
"size": abs(size),
|
||||
"entry_price": float(item.get("entryPrice") or item.get("avgCost") or item.get("avgPx") or 0),
|
||||
"unrealized_pnl": float(item.get("unRealizedProfit") or item.get("upl") or item.get("unrealisedPnl") or 0),
|
||||
"leverage": float(item.get("leverage") or 1),
|
||||
"mark_price": float(item.get("markPrice") or item.get("markPx") or 0),
|
||||
"entry_price": float(item.get("entryPrice") or item.get("avgCost") or item.get("avgPx") or item.get("avgPx") or 0),
|
||||
"unrealized_pnl": float(item.get("unRealizedProfit") or item.get("upl") or item.get("unrealisedPnl") or item.get("pnl") or 0),
|
||||
"leverage": float(item.get("leverage") or item.get("lever") or 1),
|
||||
"mark_price": float(item.get("markPrice") or item.get("markPx") or item.get("last") or 0),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"_parse_positions error: {e}")
|
||||
return result
|
||||
|
||||
|
||||
@quick_trade_bp.route('/close-position', methods=['POST'])
|
||||
@login_required
|
||||
def close_position():
|
||||
"""
|
||||
Close an existing position.
|
||||
|
||||
Body JSON:
|
||||
credential_id (int) — saved exchange credential ID
|
||||
symbol (str) — e.g. "BTC/USDT"
|
||||
market_type (str) — "swap" / "spot" (default: swap)
|
||||
size (float) — position size to close (optional, defaults to full position)
|
||||
source (str) — "ai_radar" / "ai_analysis" / "indicator" / "manual"
|
||||
"""
|
||||
try:
|
||||
user_id = g.user_id
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
|
||||
credential_id = int(body.get("credential_id") or 0)
|
||||
symbol = str(body.get("symbol") or "").strip()
|
||||
market_type = str(body.get("market_type") or "swap").strip().lower()
|
||||
close_size = float(body.get("size") or 0) # 0 means close full position
|
||||
source = str(body.get("source") or "manual").strip()
|
||||
|
||||
# ---- validation ----
|
||||
if not credential_id:
|
||||
return jsonify({"code": 0, "msg": "Missing credential_id"}), 400
|
||||
if not symbol:
|
||||
return jsonify({"code": 0, "msg": "Missing symbol"}), 400
|
||||
|
||||
if market_type in ("futures", "future", "perp", "perpetual"):
|
||||
market_type = "swap"
|
||||
|
||||
# ---- build exchange client ----
|
||||
exchange_config = _build_exchange_config(credential_id, user_id, {
|
||||
"market_type": market_type,
|
||||
})
|
||||
exchange_id = (exchange_config.get("exchange_id") or "").strip().lower()
|
||||
if not exchange_id:
|
||||
return jsonify({"code": 0, "msg": "Invalid credential: missing exchange_id"}), 400
|
||||
|
||||
client = _create_client(exchange_config, market_type=market_type)
|
||||
|
||||
# ---- get current position ----
|
||||
positions = []
|
||||
try:
|
||||
from app.services.live_trading.okx import OkxClient
|
||||
if isinstance(client, OkxClient):
|
||||
from app.services.live_trading.symbols import to_okx_swap_inst_id, to_okx_spot_inst_id
|
||||
if market_type == "spot":
|
||||
inst_id = to_okx_spot_inst_id(symbol)
|
||||
else:
|
||||
inst_id = to_okx_swap_inst_id(symbol)
|
||||
raw = client.get_positions(inst_id=inst_id)
|
||||
positions = _parse_positions(raw)
|
||||
elif hasattr(client, "get_positions"):
|
||||
raw = client.get_positions(symbol=symbol)
|
||||
positions = _parse_positions(raw)
|
||||
elif hasattr(client, "get_position"):
|
||||
raw = client.get_position(symbol=symbol)
|
||||
positions = _parse_positions(raw)
|
||||
except Exception as pe:
|
||||
logger.warning(f"Position fetch failed: {pe}")
|
||||
|
||||
if not positions:
|
||||
return jsonify({"code": 0, "msg": f"No position found for {symbol}"}), 404
|
||||
|
||||
# Find matching position for this symbol
|
||||
position = None
|
||||
for pos in positions:
|
||||
pos_symbol = pos.get("symbol", "").strip()
|
||||
# Match by symbol (may need normalization)
|
||||
if symbol.upper().replace("/", "") in pos_symbol.upper().replace("/", "").replace("-", ""):
|
||||
position = pos
|
||||
break
|
||||
|
||||
if not position:
|
||||
return jsonify({"code": 0, "msg": f"No position found for {symbol}"}), 404
|
||||
|
||||
position_side = str(position.get("side") or "").strip().lower()
|
||||
position_size = float(position.get("size") or 0)
|
||||
|
||||
if position_size <= 0:
|
||||
return jsonify({"code": 0, "msg": "Position size is zero or invalid"}), 400
|
||||
|
||||
# Determine close size
|
||||
actual_close_size = close_size if close_size > 0 else position_size
|
||||
if actual_close_size > position_size:
|
||||
actual_close_size = position_size
|
||||
|
||||
# ---- determine signal type based on position side ----
|
||||
if market_type == "spot":
|
||||
# Spot only supports long positions
|
||||
if position_side != "long":
|
||||
return jsonify({"code": 0, "msg": "Spot market only supports closing long positions"}), 400
|
||||
signal_type = "close_long"
|
||||
else:
|
||||
# Swap: close_long or close_short
|
||||
if position_side == "long":
|
||||
signal_type = "close_long"
|
||||
elif position_side == "short":
|
||||
signal_type = "close_short"
|
||||
else:
|
||||
return jsonify({"code": 0, "msg": f"Unknown position side: {position_side}"}), 400
|
||||
|
||||
# ---- place close order ----
|
||||
from app.services.live_trading.execution import place_order_from_signal
|
||||
|
||||
# Generate client_order_id
|
||||
timestamp_suffix = str(int(time.time()))[-6:]
|
||||
uuid_suffix = uuid.uuid4().hex[:8]
|
||||
client_order_id = f"qtc{timestamp_suffix}{uuid_suffix}" # 'c' for close
|
||||
|
||||
result = place_order_from_signal(
|
||||
client=client,
|
||||
signal_type=signal_type,
|
||||
symbol=symbol,
|
||||
amount=actual_close_size, # Use position size directly (already in base qty)
|
||||
market_type=market_type,
|
||||
exchange_config=exchange_config,
|
||||
client_order_id=client_order_id,
|
||||
)
|
||||
|
||||
# ---- extract result ----
|
||||
exchange_order_id = str(getattr(result, "exchange_order_id", "") or "")
|
||||
filled = float(getattr(result, "filled", 0) or 0)
|
||||
avg_fill = float(getattr(result, "avg_price", 0) or 0)
|
||||
raw = getattr(result, "raw", {}) or {}
|
||||
|
||||
# ---- record trade ----
|
||||
trade_id = _record_quick_trade(
|
||||
user_id=user_id,
|
||||
credential_id=credential_id,
|
||||
exchange_id=exchange_id,
|
||||
symbol=symbol,
|
||||
side="sell" if position_side == "long" else "buy", # Opposite of position side
|
||||
order_type="market",
|
||||
amount=actual_close_size, # Record position size
|
||||
price=avg_fill,
|
||||
leverage=float(position.get("leverage") or 1),
|
||||
market_type=market_type,
|
||||
tp_price=0,
|
||||
sl_price=0,
|
||||
status="filled" if filled > 0 else "submitted",
|
||||
exchange_order_id=exchange_order_id,
|
||||
filled=filled,
|
||||
avg_price=avg_fill,
|
||||
error_msg="",
|
||||
source=source,
|
||||
raw_result=raw,
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"code": 1,
|
||||
"msg": "Position closed successfully",
|
||||
"data": {
|
||||
"trade_id": trade_id,
|
||||
"exchange_order_id": exchange_order_id,
|
||||
"filled": filled,
|
||||
"avg_price": avg_fill,
|
||||
"closed_size": actual_close_size,
|
||||
"position_side": position_side,
|
||||
"status": "filled" if filled > 0 else "submitted",
|
||||
},
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"close_position failed: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return jsonify({"code": 0, "msg": str(e)}), 500
|
||||
|
||||
|
||||
@quick_trade_bp.route('/history', methods=['GET'])
|
||||
@login_required
|
||||
def get_history():
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Translate a strategy signal into a direct-exchange order call.
|
||||
|
||||
Supports:
|
||||
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex
|
||||
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex, Deepcoin
|
||||
- Traditional brokers: Interactive Brokers (IBKR) for US stocks
|
||||
- Forex brokers: MetaTrader 5 (MT5)
|
||||
"""
|
||||
@@ -26,6 +26,9 @@ from app.services.live_trading.kucoin import KucoinFuturesClient
|
||||
from app.services.live_trading.gate import GateSpotClient, GateUsdtFuturesClient
|
||||
from app.services.live_trading.bitfinex import BitfinexClient, BitfinexDerivativesClient
|
||||
|
||||
# Lazy import Deepcoin
|
||||
DeepcoinClient = None
|
||||
|
||||
# Lazy import IBKR
|
||||
IBKRClient = None
|
||||
|
||||
@@ -94,6 +97,7 @@ def place_order_from_signal(
|
||||
side=side,
|
||||
pos_side=pos_side,
|
||||
size=qty,
|
||||
market_type=mt,
|
||||
td_mode=str(td_mode),
|
||||
reduce_only=reduce_only,
|
||||
client_order_id=client_order_id,
|
||||
@@ -155,6 +159,25 @@ def place_order_from_signal(
|
||||
if isinstance(client, KrakenFuturesClient):
|
||||
return client.place_market_order(symbol=symbol, side=side, size=qty, reduce_only=reduce_only, client_order_id=client_order_id)
|
||||
|
||||
# Check for Deepcoin client (lazy import to avoid circular dependency)
|
||||
global DeepcoinClient
|
||||
if DeepcoinClient is None:
|
||||
try:
|
||||
from app.services.live_trading.deepcoin import DeepcoinClient as _DeepcoinClient
|
||||
DeepcoinClient = _DeepcoinClient
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if DeepcoinClient is not None and isinstance(client, DeepcoinClient):
|
||||
return client.place_market_order(
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
qty=qty,
|
||||
reduce_only=reduce_only,
|
||||
pos_side=pos_side,
|
||||
client_order_id=client_order_id,
|
||||
)
|
||||
|
||||
# Check for IBKR client (lazy import to avoid circular dependency)
|
||||
global IBKRClient
|
||||
if IBKRClient is None:
|
||||
|
||||
@@ -294,21 +294,79 @@ class OkxClient(BaseRestClient):
|
||||
if params:
|
||||
# OKX expects the query string in the signed request path. Keep key order stable.
|
||||
# Convert all values to string to avoid "True"/"False" surprises.
|
||||
norm = {str(k): "" if v is None else str(v) for k, v in dict(params).items()}
|
||||
qs = urlencode(sorted(norm.items()), doseq=True)
|
||||
# Filter out empty strings and None values (OKX doesn't like empty params)
|
||||
norm = {str(k): str(v) for k, v in dict(params).items() if v is not None and str(v).strip() != ""}
|
||||
if norm:
|
||||
# Sort by key to ensure consistent ordering (OKX requirement)
|
||||
qs = urlencode(sorted(norm.items()), doseq=True)
|
||||
|
||||
signed_path = f"{path}?{qs}" if qs else path
|
||||
sign = self._sign(ts, method, signed_path, body_str)
|
||||
|
||||
# For GET requests with query params, we need to ensure the actual request URL matches the signed path
|
||||
# OKX requires exact match between signed path and actual request path
|
||||
if method.upper() == "GET" and qs:
|
||||
# Append query string directly to path to match signature exactly
|
||||
# Don't use params parameter to avoid double encoding
|
||||
request_path = f"{path}?{qs}"
|
||||
request_params = None
|
||||
else:
|
||||
request_path = path
|
||||
request_params = params
|
||||
|
||||
code, data, text = self._request(
|
||||
method,
|
||||
path,
|
||||
params=params,
|
||||
request_path,
|
||||
params=request_params,
|
||||
data=body_str if body_str else None,
|
||||
headers=self._headers(ts, sign),
|
||||
)
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"OKX HTTP {code}: {text[:500]}")
|
||||
# Provide more helpful error messages for common permission issues
|
||||
error_msg = text[:500] if text else f"HTTP {code}"
|
||||
if code == 401:
|
||||
error_code = ""
|
||||
if isinstance(data, dict):
|
||||
error_code = str(data.get("code") or "")
|
||||
if error_code == "50120" or "permission" in error_msg.lower():
|
||||
raise LiveTradingError(
|
||||
f"OKX API permission error (HTTP {code}, code {error_code}): {error_msg}\n"
|
||||
f"Solution: Please enable 'Trade' permission for your API key in OKX account.\n"
|
||||
f"Path: OKX website -> API Management -> Edit API Key -> Enable 'Trade' permission"
|
||||
)
|
||||
raise LiveTradingError(f"OKX HTTP {code}: {error_msg}")
|
||||
if isinstance(data, dict) and str(data.get("code") or "") not in ("0", ""):
|
||||
error_code = str(data.get("code") or "")
|
||||
error_msg = str(data.get("msg") or data)
|
||||
|
||||
# Check for specific error codes in data array
|
||||
data_array = data.get("data", [])
|
||||
if isinstance(data_array, list) and data_array:
|
||||
first_item = data_array[0] if isinstance(data_array[0], dict) else {}
|
||||
s_code = str(first_item.get("sCode") or "")
|
||||
s_msg = str(first_item.get("sMsg") or "")
|
||||
|
||||
# Error code 51008: Insufficient margin
|
||||
if s_code == "51008" or "insufficient" in s_msg.lower() or "margin" in s_msg.lower():
|
||||
raise LiveTradingError(
|
||||
f"OKX insufficient margin error (code {s_code}): {s_msg}\n"
|
||||
f"Solution: Please ensure you have sufficient USDT margin in your account to place this order."
|
||||
)
|
||||
# Error code 50120: Permission error
|
||||
if s_code == "50120" or error_code == "50120" or "permission" in str(error_msg).lower():
|
||||
raise LiveTradingError(
|
||||
f"OKX API permission error (code {s_code or error_code}): {s_msg or error_msg}\n"
|
||||
f"Solution: Please enable 'Trade' permission for your API key in OKX account.\n"
|
||||
f"Path: OKX website -> API Management -> Edit API Key -> Enable 'Trade' permission"
|
||||
)
|
||||
|
||||
# Fallback for permission errors
|
||||
if error_code == "50120" or "permission" in str(error_msg).lower():
|
||||
raise LiveTradingError(
|
||||
f"OKX API permission error (code {error_code}): {error_msg}\n"
|
||||
f"Solution: Please enable 'Trade' permission for your API key in OKX account.\n"
|
||||
f"Path: OKX website -> API Management -> Edit API Key -> Enable 'Trade' permission"
|
||||
)
|
||||
raise LiveTradingError(f"OKX error: {data}")
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
|
||||
@@ -316,21 +374,44 @@ class OkxClient(BaseRestClient):
|
||||
code, data, _ = self._request("GET", "/api/v5/public/time")
|
||||
return code == 200 and isinstance(data, dict)
|
||||
|
||||
def get_ticker(self, *, inst_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get ticker price for an instrument.
|
||||
|
||||
Endpoint: GET /api/v5/market/ticker?instId=...
|
||||
"""
|
||||
if not inst_id:
|
||||
return {}
|
||||
raw = self._public_request("GET", "/api/v5/market/ticker", params={"instId": inst_id})
|
||||
data = (raw.get("data") or []) if isinstance(raw, dict) else []
|
||||
first: Dict[str, Any] = data[0] if isinstance(data, list) and data else {}
|
||||
return first if isinstance(first, dict) else {}
|
||||
|
||||
def get_balance(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Private endpoint to validate credentials (best-effort).
|
||||
"""
|
||||
return self._signed_request("GET", "/api/v5/account/balance")
|
||||
|
||||
def get_positions(self, *, inst_id: str = "") -> Dict[str, Any]:
|
||||
def get_positions(self, *, inst_id: str = "", inst_type: str = "SWAP") -> Dict[str, Any]:
|
||||
"""
|
||||
Get swap positions (best-effort).
|
||||
Get positions (best-effort).
|
||||
|
||||
Args:
|
||||
inst_id: Instrument ID (optional, for filtering)
|
||||
inst_type: Instrument type - "SPOT" or "SWAP" (default: "SWAP")
|
||||
|
||||
Endpoint: GET /api/v5/account/positions
|
||||
"""
|
||||
params: Dict[str, Any] = {"instType": "SWAP"}
|
||||
if inst_id:
|
||||
params["instId"] = str(inst_id)
|
||||
# Validate inst_type
|
||||
it = str(inst_type or "SWAP").strip().upper()
|
||||
if it not in ("SPOT", "SWAP", "FUTURES", "OPTION"):
|
||||
it = "SWAP"
|
||||
|
||||
params: Dict[str, Any] = {"instType": it}
|
||||
# Only add instId if it's not empty
|
||||
if inst_id and str(inst_id).strip():
|
||||
params["instId"] = str(inst_id).strip()
|
||||
return self._signed_request("GET", "/api/v5/account/positions", params=params)
|
||||
|
||||
def set_leverage(self, *, inst_id: str, lever: float, mgn_mode: str = "cross", pos_side: str = "") -> bool:
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+69
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 21</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
|
||||
<g id="Group-21" transform="translate(77.000000, 73.000000)">
|
||||
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
|
||||
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
|
||||
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
|
||||
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
|
||||
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
|
||||
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
|
||||
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
|
||||
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
|
||||
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
|
||||
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
|
||||
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
|
||||
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
|
||||
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
|
||||
</g>
|
||||
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
|
||||
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
|
||||
</g>
|
||||
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
|
||||
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
|
||||
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
|
||||
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
|
||||
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
|
||||
</g>
|
||||
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
|
||||
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
|
||||
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||
</g>
|
||||
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
|
||||
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
|
||||
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
|
||||
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
|
||||
</g>
|
||||
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
|
||||
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
|
||||
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
|
||||
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
|
||||
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
|
||||
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
|
||||
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
|
||||
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
|
||||
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Vendored
+424
@@ -0,0 +1,424 @@
|
||||
<!doctype html><html lang="zh-cmn-Hans"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/slogo.png"><title>QuantDinger</title><style>.first-loading-wrp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
min-height: 420px;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.first-loading-wrp > h2 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 40px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.first-loading-wrp .loading-wrp {
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
/* 像素风格小猫奔跑动画容器 */
|
||||
.pixel-cat-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
margin: 0 auto 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 像素小猫主体 */
|
||||
.pixel-cat {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 24px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
animation: catRun 0.4s steps(2) infinite, catMove 3.5s linear infinite;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
/* 所有像素元素都使用锐利边缘 */
|
||||
.pixel-cat * {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
/* 猫头 - 像素方块组成 */
|
||||
.cat-head {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
background:
|
||||
/* 头部主体白色 */
|
||||
linear-gradient(#fff, #fff) 2px 4px / 12px 10px no-repeat,
|
||||
/* 左半脸黑色斑块 */
|
||||
linear-gradient(#000, #000) 2px 4px / 6px 10px no-repeat,
|
||||
/* 头顶 */
|
||||
linear-gradient(#fff, #fff) 4px 2px / 8px 2px no-repeat;
|
||||
animation: headBob 0.4s steps(2) infinite;
|
||||
}
|
||||
|
||||
/* 左耳 - 黑色尖耳朵 */
|
||||
.cat-ear-left {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: -2px;
|
||||
width: 4px;
|
||||
height: 6px;
|
||||
background:
|
||||
linear-gradient(#000, #000) 0px 4px / 4px 2px no-repeat,
|
||||
linear-gradient(#000, #000) 1px 2px / 2px 2px no-repeat,
|
||||
linear-gradient(#000, #000) 1px 0px / 2px 2px no-repeat;
|
||||
}
|
||||
|
||||
/* 右耳 - 白色尖耳朵 */
|
||||
.cat-ear-right {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -2px;
|
||||
width: 4px;
|
||||
height: 6px;
|
||||
background:
|
||||
linear-gradient(#fff, #fff) 0px 4px / 4px 2px no-repeat,
|
||||
linear-gradient(#fff, #fff) 1px 2px / 2px 2px no-repeat,
|
||||
linear-gradient(#fff, #fff) 1px 0px / 2px 2px no-repeat;
|
||||
box-shadow: inset 0 0 0 1px #000;
|
||||
}
|
||||
|
||||
.cat-ear-right::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 6px;
|
||||
border: 1px solid #000;
|
||||
border-width: 0 1px 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 左眼 - 黑底白眼(在黑色区域) */
|
||||
.cat-eye-left {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 6px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cat-eye-left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* 右眼 - 白底黑眼(在白色区域) */
|
||||
.cat-eye-right {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 6px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: #fff;
|
||||
border: 1px solid #000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cat-eye-right::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
top: 0px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* 鼻子 - 小粉色方块 */
|
||||
.cat-nose {
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 10px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* 胡须 - 像素线条 */
|
||||
.cat-whiskers {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 10px;
|
||||
width: 16px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.cat-whiskers::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 1px;
|
||||
background: #000;
|
||||
box-shadow: 0 2px 0 #000;
|
||||
}
|
||||
|
||||
.cat-whiskers::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 1px;
|
||||
background: #000;
|
||||
box-shadow: 0 2px 0 #000;
|
||||
}
|
||||
|
||||
/* 身体 - 黑白相间像素块 */
|
||||
.cat-body {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 14px;
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
background:
|
||||
/* 白色部分 */
|
||||
linear-gradient(#fff, #fff) 6px 0 / 8px 10px no-repeat,
|
||||
/* 黑色部分 */
|
||||
linear-gradient(#000, #000) 0 0 / 8px 10px no-repeat;
|
||||
border: 1px solid #000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 前腿 - 左 (黑色) */
|
||||
.cat-leg-front-left {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 22px;
|
||||
width: 3px;
|
||||
height: 8px;
|
||||
background: #000;
|
||||
animation: legFront 0.2s steps(2) infinite;
|
||||
}
|
||||
|
||||
/* 前腿 - 右 (白色带边框) */
|
||||
.cat-leg-front-right {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 22px;
|
||||
width: 3px;
|
||||
height: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #000;
|
||||
box-sizing: border-box;
|
||||
animation: legFront 0.2s steps(2) infinite 0.1s;
|
||||
}
|
||||
|
||||
/* 后腿 - 左 (白色带边框) */
|
||||
.cat-leg-back-left {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 22px;
|
||||
width: 3px;
|
||||
height: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #000;
|
||||
box-sizing: border-box;
|
||||
animation: legBack 0.2s steps(2) infinite 0.1s;
|
||||
}
|
||||
|
||||
/* 后腿 - 右 (黑色) */
|
||||
.cat-leg-back-right {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 22px;
|
||||
width: 3px;
|
||||
height: 8px;
|
||||
background: #000;
|
||||
animation: legBack 0.2s steps(2) infinite;
|
||||
}
|
||||
|
||||
/* 尾巴 - 长且弯曲的像素尾巴 */
|
||||
.cat-tail {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 10px;
|
||||
width: 12px;
|
||||
height: 10px;
|
||||
background:
|
||||
/* 尾巴根部 */
|
||||
linear-gradient(#000, #000) 0 6px / 3px 3px no-repeat,
|
||||
/* 尾巴中部 */
|
||||
linear-gradient(#000, #000) 3px 4px / 3px 3px no-repeat,
|
||||
/* 尾巴弯曲 */
|
||||
linear-gradient(#000, #000) 6px 2px / 3px 3px no-repeat,
|
||||
/* 尾巴尖端 */
|
||||
linear-gradient(#000, #000) 9px 0 / 3px 3px no-repeat;
|
||||
animation: tailWag 0.3s steps(2) infinite alternate;
|
||||
transform-origin: left bottom;
|
||||
}
|
||||
|
||||
/* 奔跑动画 - 轻微上下跳动 */
|
||||
@keyframes catRun {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-3px); }
|
||||
}
|
||||
|
||||
/* 移动动画 - 左右移动 */
|
||||
@keyframes catMove {
|
||||
0% { left: -40px; }
|
||||
100% { left: calc(100% + 40px); }
|
||||
}
|
||||
|
||||
/* 前腿动画 */
|
||||
@keyframes legFront {
|
||||
0%, 100% { transform: rotate(-15deg); }
|
||||
50% { transform: rotate(15deg); }
|
||||
}
|
||||
|
||||
/* 后腿动画 */
|
||||
@keyframes legBack {
|
||||
0%, 100% { transform: rotate(15deg); }
|
||||
50% { transform: rotate(-15deg); }
|
||||
}
|
||||
|
||||
/* 尾巴摆动 */
|
||||
@keyframes tailWag {
|
||||
0% { transform: rotate(-10deg); }
|
||||
100% { transform: rotate(10deg); }
|
||||
}
|
||||
|
||||
/* 头部轻微摆动 */
|
||||
@keyframes headBob {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-1px); }
|
||||
}
|
||||
|
||||
/* 地面效果 - 像素风格 */
|
||||
.ground {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 12px;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
#222 0px,
|
||||
#222 6px,
|
||||
#555 6px,
|
||||
#555 12px
|
||||
);
|
||||
animation: groundMove 0.3s linear infinite;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
@keyframes groundMove {
|
||||
0% { background-position: 0 0; }
|
||||
100% { background-position: 12px 0; }
|
||||
}
|
||||
|
||||
/* 品牌文字 */
|
||||
.brand-text {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-top: 20px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.first-loading-wrp {
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.first-loading-wrp > h2 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ground {
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
#555 0px,
|
||||
#555 6px,
|
||||
#333 6px,
|
||||
#333 12px
|
||||
);
|
||||
}
|
||||
|
||||
/* 暗色模式下白色部分改成浅灰 */
|
||||
.cat-head {
|
||||
background:
|
||||
linear-gradient(#ddd, #ddd) 2px 4px / 12px 10px no-repeat,
|
||||
linear-gradient(#000, #000) 2px 4px / 6px 10px no-repeat,
|
||||
linear-gradient(#ddd, #ddd) 4px 2px / 8px 2px no-repeat;
|
||||
}
|
||||
|
||||
.cat-body {
|
||||
background:
|
||||
linear-gradient(#ddd, #ddd) 6px 0 / 8px 10px no-repeat,
|
||||
linear-gradient(#000, #000) 0 0 / 8px 10px no-repeat;
|
||||
}
|
||||
|
||||
.cat-leg-front-right,
|
||||
.cat-leg-back-left {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.cat-eye-left,
|
||||
.cat-eye-right {
|
||||
background: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保像素风格在所有浏览器中正确显示 */
|
||||
.pixel-cat,
|
||||
.pixel-cat * {
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -webkit-crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
/* 手机端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.pixel-cat-container {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.first-loading-wrp > h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
}</style><script defer="defer" src="/js/chunk-vendors.6f65f877.js" type="module"></script><script defer="defer" src="/js/app.78319175.js" type="module"></script><link href="/css/chunk-vendors.b8cb9e53.css" rel="stylesheet"><link href="/css/app.b8761398.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.9d679269.js" nomodule></script><script defer="defer" src="/js/app-legacy.b9bb3b91.js" nomodule></script></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><div class="first-loading-wrp"><h2>Landing</h2><div class="loading-wrp"><div class="pixel-cat-container"><div class="ground"></div><div class="pixel-cat"><div class="cat-head"><div class="cat-ear-left"></div><div class="cat-ear-right"></div><div class="cat-eye-left"></div><div class="cat-eye-right"></div><div class="cat-nose"></div><div class="cat-whiskers"></div></div><div class="cat-body"></div><div class="cat-leg-front-left"></div><div class="cat-leg-front-right"></div><div class="cat-leg-back-left"></div><div class="cat-leg-back-right"></div><div class="cat-tail"></div></div></div></div><div class="brand-text">QuantDinger</div></div></div></body></html>
|
||||
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+18
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+16
File diff suppressed because one or more lines are too long
Vendored
+16
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
"use strict";(self["webpackChunkvue_antd_pro"]=self["webpackChunkvue_antd_pro"]||[]).push([[824],{95824:function(t,n,r){r.r(n),r.d(n,{changePassword:function(){return p},getGitHubOAuthUrl:function(){return g},getGoogleOAuthUrl:function(){return l},getSecurityConfig:function(){return o},getUserInfo:function(){return c},login:function(){return i},loginWithCode:function(){return s},logout:function(){return a},register:function(){return f},resetPassword:function(){return d},sendVerificationCode:function(){return h}});r(34782),r(27495),r(99449),r(25440),r(11392),r(42762);var u=r(75769);function e(t){var n="/api".trim(),r=t.startsWith("/")?t:"/".concat(t);if(!n)return r;var u=n.replace(/\/+$/,"");return u.endsWith("/api")&&r.startsWith("/api/")?u+r.slice(4):u+r}function o(){return(0,u.Ay)({url:"/api/auth/security-config",method:"get"})}function i(t){return(0,u.Ay)({url:"/api/auth/login",method:"post",data:t})}function a(){return(0,u.Ay)({url:"/api/auth/logout",method:"post"})}function c(){return(0,u.Ay)({url:"/api/auth/info",method:"get"})}function h(t){return(0,u.Ay)({url:"/api/auth/send-code",method:"post",data:t})}function s(t){return(0,u.Ay)({url:"/api/auth/login-code",method:"post",data:t})}function f(t){return(0,u.Ay)({url:"/api/auth/register",method:"post",data:t})}function d(t){return(0,u.Ay)({url:"/api/auth/reset-password",method:"post",data:t})}function p(t){return(0,u.Ay)({url:"/api/auth/change-password",method:"post",data:t})}function l(){return e("/api/auth/oauth/google")}function g(){return e("/api/auth/oauth/github")}}}]);
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
"use strict";(self["webpackChunkvue_antd_pro"]=self["webpackChunkvue_antd_pro"]||[]).push([[824],{95824:function(t,n,r){r.r(n),r.d(n,{changePassword:function(){return p},getGitHubOAuthUrl:function(){return g},getGoogleOAuthUrl:function(){return l},getSecurityConfig:function(){return o},getUserInfo:function(){return c},login:function(){return i},loginWithCode:function(){return s},logout:function(){return a},register:function(){return f},resetPassword:function(){return d},sendVerificationCode:function(){return h}});var u=r(75769);function e(t){var n="/api".trim(),r=t.startsWith("/")?t:"/".concat(t);if(!n)return r;var u=n.replace(/\/+$/,"");return u.endsWith("/api")&&r.startsWith("/api/")?u+r.slice(4):u+r}function o(){return(0,u.Ay)({url:"/api/auth/security-config",method:"get"})}function i(t){return(0,u.Ay)({url:"/api/auth/login",method:"post",data:t})}function a(){return(0,u.Ay)({url:"/api/auth/logout",method:"post"})}function c(){return(0,u.Ay)({url:"/api/auth/info",method:"get"})}function h(t){return(0,u.Ay)({url:"/api/auth/send-code",method:"post",data:t})}function s(t){return(0,u.Ay)({url:"/api/auth/login-code",method:"post",data:t})}function f(t){return(0,u.Ay)({url:"/api/auth/register",method:"post",data:t})}function d(t){return(0,u.Ay)({url:"/api/auth/reset-password",method:"post",data:t})}function p(t){return(0,u.Ay)({url:"/api/auth/change-password",method:"post",data:t})}function l(){return e("/api/auth/oauth/google")}function g(){return e("/api/auth/oauth/github")}}}]);
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+308
File diff suppressed because one or more lines are too long
+308
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
"use strict";(self["webpackChunkvue_antd_pro"]=self["webpackChunkvue_antd_pro"]||[]).push([[143],{32319:function(t,e,n){n.r(e),n.d(e,{default:function(){return l}});var u=function(){var t=this,e=t._self._c;return e("a-result",{attrs:{status:"404",title:"404","sub-title":"Sorry, the page you visited does not exist."},scopedSlots:t._u([{key:"extra",fn:function(){return[e("a-button",{attrs:{type:"primary"},on:{click:t.toHome}},[t._v(" Back Home ")])]},proxy:!0}])})},o=[],r={name:"Exception404",methods:{toHome:function(){this.$router.push({path:"/"})}}},s=r,a=n(81656),i=(0,a.A)(s,u,o,!1,null,null,null),l=i.exports}}]);
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
"use strict";(self["webpackChunkvue_antd_pro"]=self["webpackChunkvue_antd_pro"]||[]).push([[143],{32319:function(t,e,n){n.r(e),n.d(e,{default:function(){return l}});var u=function(){var t=this,e=t._self._c;return e("a-result",{attrs:{status:"404",title:"404","sub-title":"Sorry, the page you visited does not exist."},scopedSlots:t._u([{key:"extra",fn:function(){return[e("a-button",{attrs:{type:"primary"},on:{click:t.toHome}},[t._v(" Back Home ")])]},proxy:!0}])})},o=[],r={name:"Exception404",methods:{toHome:function(){this.$router.push({path:"/"})}}},s=r,a=n(81656),i=(0,a.A)(s,u,o,!1,null,null,null),l=i.exports}}]);
|
||||
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Reference in New Issue
Block a user