This commit is contained in:
Flatlogic Bot 2026-02-06 12:45:13 +00:00
parent 65d8cd957f
commit eb60696dd9
13 changed files with 977 additions and 63 deletions

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{{ site_settings.site_name|default:"BitCrypto" }} - 移动版</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<style>
body { background-color: #0b0e11; color: #eaecef; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; overflow-x: hidden; }
.mobile-nav { position: fixed; bottom: 0; left: 0; right: 0; background: #161a1e; display: flex; justify-content: space-around; padding: 10px 0; border-top: 1px solid #2b3139; z-index: 1000; }
.nav-item { text-align: center; color: #848e9c; text-decoration: none; font-size: 10px; }
.nav-item.active { color: #f0b90b; }
.nav-item i { font-size: 20px; display: block; margin-bottom: 2px; }
.glass-card { background: #161a1e; border: 1px solid #2b3139; border-radius: 8px; margin-bottom: 10px; }
.x-small { font-size: 11px; }
.text-warning { color: #f0b90b !important; }
.btn-warning { background-color: #f0b90b; border-color: #f0b90b; color: #000; }
.form-control, .input-group-text { background-color: #1e2329 !important; border-color: #2b3139 !important; color: white !important; }
.custom-range::-webkit-slider-thumb { background: #f0b90b; cursor: pointer; height: 16px; width: 16px; }
.custom-range-danger::-webkit-slider-thumb { background: #f6465d; cursor: pointer; height: 16px; width: 16px; }
body { padding-bottom: 70px; }
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="p-2">
{% block content %}{% endblock %}
</div>
<div class="mobile-nav">
<a href="{% url 'core:index' %}" class="nav-item {% if request.resolver_match.url_name == 'index' %}active{% endif %}">
<i class="bi bi-house-door"></i>首页
</a>
<a href="{% url 'core:market_center' %}" class="nav-item {% if request.resolver_match.url_name == 'market_center' %}active{% endif %}">
<i class="bi bi-graph-up"></i>行情
</a>
<a href="{% url 'core:trade' 'spot' %}" class="nav-item {% if 'trade' in request.resolver_match.url_name %}active{% endif %}">
<i class="bi bi-arrow-left-right"></i>交易
</a>
<a href="{% url 'core:profile' %}" class="nav-item {% if request.resolver_match.url_name == 'profile' %}active{% endif %}">
<i class="bi bi-person"></i>我的
</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,44 @@
{% extends 'core/mobile/base_mobile.html' %}
{% block content %}
<div class="p-3">
<div class="mb-4">
<a href="{% url 'core:profile' %}" class="text-decoration-none text-secondary small"><i class="bi bi-chevron-left"></i> 返回</a>
<h4 class="fw-bold mt-2">充值 USDT</h4>
</div>
<div class="glass-card p-3 mb-3">
<div class="text-secondary x-small mb-2">选择币种</div>
<div class="bg-dark p-2 rounded d-flex justify-content-between align-items-center mb-3">
<span class="fw-bold">USDT</span>
<span class="badge bg-warning text-dark">TRC20</span>
</div>
<div class="text-center py-4 bg-white rounded mb-3">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=TFA7S7S7S7S7S7S7S7S7S7S7S7S7S7" alt="QR" style="width: 120px;">
</div>
<div class="text-secondary x-small mb-1">充值地址</div>
<div class="input-group input-group-sm mb-3">
<input type="text" class="form-control" value="TFA7S7S7S7S7S7S7S7S7S7S7S7S7S7" readonly>
<button class="btn btn-warning" onclick="alert('已复制')">复制</button>
</div>
<div class="alert alert-dark x-small py-2">
<i class="bi bi-info-circle me-1"></i> 请务必确认网络为 TRC20否则资产将无法找回。
</div>
</div>
<form method="POST" class="glass-card p-3">
{% csrf_token %}
<div class="mb-3">
<label class="form-label small text-secondary">充值金额</label>
<input type="number" name="amount" class="form-control" placeholder="请输入充值金额" required>
</div>
<div class="mb-3">
<label class="form-label small text-secondary">交易哈希 (TXID)</label>
<input type="text" name="tx_id" class="form-control" placeholder="请输入流水号" required>
</div>
<button type="submit" class="btn btn-warning w-100 fw-bold">提交充值申请</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,88 @@
{% extends 'core/mobile/base_mobile.html' %}
{% load static %}
{% block content %}
<div class="glass-card p-3 text-center mb-3" style="background: linear-gradient(135deg, #161a1e 0%, #1e2329 100%);">
<img src="{% static 'images/logo.png' %}" alt="Logo" style="height: 40px;" class="mb-2">
<h4 class="fw-bold">全球领先的数字资产交易平台</h4>
<p class="text-secondary small">安全、稳定、快捷的加密货币交易体验</p>
{% if not user.is_authenticated %}
<div class="d-grid gap-2">
<a href="{% url 'core:register' %}" class="btn btn-warning fw-bold">立即注册</a>
<a href="{% url 'core:login' %}" class="btn btn-outline-secondary btn-sm text-white">登录已有账户</a>
</div>
{% else %}
<div class="d-flex justify-content-around mt-2">
<div>
<div class="text-secondary x-small">账户总资产 (USDT)</div>
<div class="fs-5 fw-bold">{{ account.balance|add:account.frozen_balance }}</div>
</div>
</div>
{% endif %}
</div>
<div class="row g-2 mb-3">
<div class="col-4">
<a href="{% url 'core:deposit' %}" class="glass-card p-2 d-block text-center text-decoration-none text-white">
<i class="bi bi-wallet2 fs-4 text-warning"></i>
<div class="x-small mt-1">充币</div>
</a>
</div>
<div class="col-4">
<a href="{% url 'core:withdraw' %}" class="glass-card p-2 d-block text-center text-decoration-none text-white">
<i class="bi bi-cash-stack fs-4 text-info"></i>
<div class="x-small mt-1">提币</div>
</a>
</div>
<div class="col-4">
<a href="{% url 'core:verify' %}" class="glass-card p-2 d-block text-center text-decoration-none text-white">
<i class="bi bi-shield-check fs-4 text-success"></i>
<div class="x-small mt-1">认证</div>
</a>
</div>
</div>
<div class="glass-card p-2">
<div class="d-flex justify-content-between align-items-center mb-2 px-1">
<span class="fw-bold small">热门行情</span>
<a href="{% url 'core:market_center' %}" class="text-warning x-small text-decoration-none">更多 <i class="bi bi-chevron-right"></i></a>
</div>
<div id="hot-list">
<!-- JS populated -->
</div>
</div>
<style>
.hot-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 8px; border-bottom: 1px solid #2b3139; }
.hot-item:last-child { border-bottom: 0; }
</style>
{% endblock %}
{% block scripts %}
<script>
async function getHot() {
try {
const r = await fetch('/api/market_data/');
const data = await r.json();
const list = document.getElementById('hot-list');
list.innerHTML = '';
data.slice(0, 8).forEach(c => {
const base = c.symbol.replace('USDT', '');
list.innerHTML += `
<div class="hot-item" onclick="location.href='{% url 'core:trade' 'spot' %}?symbol=${c.symbol}'">
<div>
<div class="fw-bold">${base}<span class="text-secondary x-small">/USDT</span></div>
<div class="text-secondary x-small">Vol ${ (Math.random()*100).toFixed(2) }M</div>
</div>
<div class="text-end">
<div class="fw-bold">${c.price}</div>
<div class="${c.change>=0?'text-success':'text-danger'} x-small">${c.change>=0?'+':''}${c.change}%</div>
</div>
</div>`;
});
} catch(e) {}
}
getHot();
setInterval(getHot, 5000);
</script>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends 'core/mobile/base_mobile.html' %}
{% block content %}
<div class="p-3">
<div class="text-center mb-4 mt-5">
<h3 class="fw-bold">欢迎回来</h3>
<p class="text-secondary small">登录您的 {{ site_settings.site_name|default:"BitCrypto" }} 账户</p>
</div>
<form method="POST" class="glass-card p-4">
{% csrf_token %}
<div class="mb-3">
<label class="form-label small text-secondary">用户名</label>
<input type="text" name="username" class="form-control" required placeholder="请输入用户名">
</div>
<div class="mb-4">
<label class="form-label small text-secondary">登录密码</label>
<input type="password" name="password" class="form-control" required placeholder="请输入密码">
</div>
<button type="submit" class="btn btn-warning w-100 fw-bold py-2 mb-3">立即登录</button>
<div class="text-center small">
<span class="text-secondary">还没有账户?</span>
<a href="{% url 'core:register' %}" class="text-warning text-decoration-none">立即注册</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,63 @@
{% extends 'core/mobile/base_mobile.html' %}
{% block content %}
<div class="glass-card p-2 mb-2">
<div class="input-group input-group-sm">
<span class="input-group-text bg-dark border-secondary"><i class="bi bi-search text-secondary"></i></span>
<input type="text" id="market-search" class="form-control bg-dark text-white border-secondary" placeholder="搜索币种" onkeyup="filterMarkets()">
</div>
</div>
<div class="glass-card p-0">
<div class="d-flex justify-content-between p-2 text-secondary x-small border-bottom border-secondary">
<span style="width: 40%;">名称</span>
<span style="width: 30%;" class="text-end">最新价</span>
<span style="width: 30%;" class="text-end">24h 涨跌</span>
</div>
<div id="market-list">
<!-- JS populated -->
</div>
</div>
<style>
.market-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 8px; border-bottom: 1px solid #2b3139; }
</style>
{% endblock %}
{% block scripts %}
<script>
let allMarkets = [];
async function getMarkets() {
try {
const r = await fetch('/api/market_data/');
allMarkets = await r.json();
renderMarkets();
} catch(e) {}
}
function renderMarkets(filter = '') {
const list = document.getElementById('market-list');
list.innerHTML = '';
allMarkets.forEach(c => {
if (filter && !c.symbol.toLowerCase().includes(filter.toLowerCase())) return;
const base = c.symbol.replace('USDT', '');
list.innerHTML += `
<div class="market-row" onclick="location.href='{% url 'core:trade' 'spot' %}?symbol=${c.symbol}'">
<div style="width: 40%;">
<div class="fw-bold">${base}<span class="text-secondary x-small">/USDT</span></div>
</div>
<div style="width: 30%;" class="text-end fw-bold">${c.price}</div>
<div style="width: 30%;" class="text-end">
<span class="badge ${c.change>=0?'bg-success':'bg-danger'} py-1" style="min-width: 60px;">${c.change>=0?'+':''}${c.change}%</span>
</div>
</div>`;
});
}
function filterMarkets() {
renderMarkets(document.getElementById('market-search').value);
}
getMarkets();
setInterval(getMarkets, 3000);
</script>
{% endblock %}

View File

@ -0,0 +1,83 @@
{% extends 'core/mobile/base_mobile.html' %}
{% block content %}
<div class="glass-card p-3 mb-3 d-flex align-items-center">
<div class="bg-warning rounded-circle d-flex align-items-center justify-content-center text-dark fw-bold me-3" style="width: 50px; height: 50px; font-size: 20px;">
{{ user.username|first|upper }}
</div>
<div>
<div class="fw-bold">{{ user.username }}</div>
<div class="text-secondary x-small">UID: {{ account.uid }} | {{ account.get_account_type_display }}</div>
</div>
<div class="ms-auto">
<span class="badge {% if account.kyc_status == 'VERIFIED' %}bg-success{% else %}bg-secondary{% endif %} x-small">
{{ account.get_kyc_status_display }}
</span>
</div>
</div>
<div class="glass-card p-3 mb-3">
<div class="text-secondary x-small mb-1">总资产估值 (USDT)</div>
<div class="fs-3 fw-bold mb-3">{{ account.balance|add:account.frozen_balance }}</div>
<div class="row g-2">
<div class="col-6">
<div class="bg-dark p-2 rounded">
<div class="text-secondary x-small">可用</div>
<div class="fw-bold small">{{ account.balance }}</div>
</div>
</div>
<div class="col-6">
<div class="bg-dark p-2 rounded">
<div class="text-secondary x-small">冻结</div>
<div class="fw-bold small">{{ account.frozen_balance }}</div>
</div>
</div>
</div>
</div>
<div class="glass-card mb-3">
<div class="list-group list-group-flush">
<a href="{% url 'core:deposit' %}" class="list-group-item list-group-item-action bg-transparent text-white border-secondary py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-wallet2 me-3 text-warning"></i>充币</span>
<i class="bi bi-chevron-right text-secondary"></i>
</a>
<a href="{% url 'core:withdraw' %}" class="list-group-item list-group-item-action bg-transparent text-white border-secondary py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-cash-stack me-3 text-info"></i>提币</span>
<i class="bi bi-chevron-right text-secondary"></i>
</a>
<a href="{% url 'core:verify' %}" class="list-group-item list-group-item-action bg-transparent text-white border-secondary py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-shield-check me-3 text-success"></i>实名认证</span>
<i class="bi bi-chevron-right text-secondary"></i>
</a>
</div>
</div>
<div class="glass-card p-3">
<div class="fw-bold small mb-2">资产明细</div>
<table class="table table-dark table-sm x-small mb-0">
<thead>
<tr class="text-secondary">
<th>币种</th>
<th class="text-end">可用</th>
<th class="text-end">冻结</th>
</tr>
</thead>
<tbody>
{% for asset in account.assets.all %}
<tr>
<td>{{ asset.currency }}</td>
<td class="text-end">{{ asset.balance }}</td>
<td class="text-end">{{ asset.frozen }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-4 p-2">
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger w-100 btn-sm">退出登录</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends 'core/mobile/base_mobile.html' %}
{% block content %}
<div class="p-3">
<div class="text-center mb-4 mt-4">
<h3 class="fw-bold">注册账户</h3>
<p class="text-secondary small">加入 {{ site_settings.site_name|default:"BitCrypto" }},开启您的交易之旅</p>
</div>
{% if messages %}
<div class="mb-3">
{% for message in messages %}
<div class="alert alert-danger py-2 small">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<form method="POST" class="glass-card p-4">
{% csrf_token %}
<div class="mb-3">
<label class="form-label small text-secondary">用户名 / 手机号</label>
<input type="text" name="username" class="form-control" required placeholder="请输入用户名">
</div>
<div class="mb-3">
<label class="form-label small text-secondary">登录密码</label>
<input type="password" name="password" class="form-control" required placeholder="请输入密码">
</div>
<div class="mb-3">
<label class="form-label small text-secondary">确认密码</label>
<input type="password" name="password_confirm" class="form-control" required placeholder="请再次输入密码">
</div>
<div class="mb-4">
<label class="form-label small text-secondary">图形验证码</label>
<div class="input-group">
<input type="text" name="captcha_input" class="form-control" required placeholder="结果">
<span class="input-group-text bg-dark text-warning fw-bold">{{ captcha_text }}</span>
</div>
</div>
<button type="submit" class="btn btn-warning w-100 fw-bold py-2 mb-3">立即注册</button>
<div class="text-center small">
<span class="text-secondary">已有账户?</span>
<a href="{% url 'core:login' %}" class="text-warning text-decoration-none">立即登录</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,376 @@
{% extends 'core/mobile/base_mobile.html' %}
{% load static %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-2 px-1 py-1" style="background: #161a1e;">
<div class="dropdown">
<button class="btn btn-sm btn-dark dropdown-toggle fs-6 fw-bold text-warning" type="button" data-bs-toggle="dropdown">
<span id="current-symbol-display">{{ symbol }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-dark shadow-lg" id="mobile-coin-list" style="max-height: 300px; overflow-y: auto;">
<!-- JS populated -->
</ul>
</div>
<div class="d-flex gap-1 bg-dark p-1 rounded">
<a href="{% url 'core:trade' 'spot' %}?symbol={{ symbol }}" class="btn btn-xs {% if trade_type == 'SPOT' %}btn-warning text-dark{% else %}btn-dark text-secondary{% endif %} py-1 px-3 small fw-bold">现货</a>
<a href="{% url 'core:trade' 'contract' %}?symbol={{ symbol }}" class="btn btn-xs {% if trade_type == 'CONTRACT' %}btn-warning text-dark{% else %}btn-dark text-secondary{% endif %} py-1 px-3 small fw-bold">合约</a>
</div>
</div>
<div class="row g-1 mb-2">
<!-- Market Data Column -->
<div class="col-5">
<div class="glass-card p-2" style="height: 100%; font-size: 11px; background: #161a1e; border: 1px solid #2b3139;">
<div class="d-flex justify-content-between text-secondary mb-2">
<span>价格</span><span>数量</span>
</div>
<div id="m-asks" class="mb-2"></div>
<div class="text-center py-2 fw-bold text-success border-top border-bottom border-secondary my-2" id="m-current-price" style="font-size: 16px;">--</div>
<div id="m-bids"></div>
</div>
</div>
<!-- Order Form Column -->
<div class="col-7">
<div class="glass-card p-2" style="background: #161a1e; border: 1px solid #2b3139;">
{% if trade_type == 'SPOT' %}
<div class="btn-group w-100 mb-2" role="group">
<input type="radio" class="btn-check" name="trade-side" id="buy-side" checked onchange="toggleSide('BUY')">
<label class="btn btn-outline-success btn-sm py-1" for="buy-side">买入</label>
<input type="radio" class="btn-check" name="trade-side" id="sell-side" onchange="toggleSide('SELL')">
<label class="btn btn-outline-danger btn-sm py-1" for="sell-side">卖出</label>
</div>
<select class="form-select form-select-sm bg-dark text-white border-secondary mb-2" id="spot-mode-select" onchange="toggleModeMobile()">
<option value="limit">限价委托</option>
<option value="market">市价委托</option>
</select>
{% else %}
<div class="d-flex gap-2 mb-2">
<div class="dropdown w-50">
<button class="btn btn-sm btn-dark border-secondary w-100 text-warning fw-bold" id="lev-btn" data-bs-toggle="dropdown">20x</button>
<ul class="dropdown-menu dropdown-menu-dark">
<li><a class="dropdown-item" href="javascript:void(0)" onclick="setLev(10)">10x</a></li>
<li><a class="dropdown-item" href="javascript:void(0)" onclick="setLev(20)">20x</a></li>
<li><a class="dropdown-item" href="javascript:void(0)" onclick="setLev(50)">50x</a></li>
<li><a class="dropdown-item" href="javascript:void(0)" onclick="setLev(100)">100x</a></li>
</ul>
</div>
<select class="form-select form-select-sm bg-dark text-white border-secondary w-50" id="c-mode-select" onchange="toggleModeMobile()">
<option value="limit">限价</option>
<option value="market">市价</option>
</select>
</div>
{% endif %}
<div class="mb-2">
<div class="input-group input-group-sm">
<span class="input-group-text bg-dark border-secondary text-secondary" style="width: 40px; font-size: 10px;"></span>
<input type="text" id="m-price" class="form-control bg-transparent text-white border-secondary" placeholder="价格">
</div>
</div>
<div class="mb-1">
<div class="input-group input-group-sm">
<span class="input-group-text bg-dark border-secondary text-secondary" style="width: 40px; font-size: 10px;" id="m-label"></span>
<input type="number" id="m-amount" class="form-control bg-transparent text-white border-secondary" placeholder="0.00" oninput="updateSliderMobile()">
<span class="input-group-text bg-dark border-secondary text-secondary x-small" id="m-unit">{{ base_symbol }}</span>
</div>
</div>
<div class="mb-3 px-1">
<input type="range" class="form-range custom-range" id="m-slider" min="0" max="100" step="1" value="0" oninput="applySliderMobile()">
<div class="d-flex justify-content-between x-small text-secondary" style="font-size: 9px;">
<span>0%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span>
</div>
</div>
<div class="x-small mb-2 d-flex justify-content-between text-secondary" style="font-size: 10px;">
<span>可用:</span>
<span id="m-available" class="text-white">--</span>
</div>
{% if trade_type == 'CONTRACT' %}
<div class="row g-1">
<div class="col-6"><button class="btn btn-success btn-sm w-100 fw-bold" onclick="submitOrderMobile('BUY')">开多</button></div>
<div class="col-6"><button class="btn btn-danger btn-sm w-100 fw-bold" onclick="submitOrderMobile('SELL')">开空</button></div>
</div>
{% else %}
<button class="btn btn-success w-100 fw-bold btn-sm" id="m-submit-btn" onclick="submitOrderMobile(side)">买入</button>
{% endif %}
</div>
</div>
</div>
<!-- Chart Tab -->
<div class="glass-card p-1 mb-2" style="background: #161a1e; border: 1px solid #2b3139;">
<div id="tradingview_widget" style="height: 250px;"></div>
</div>
<!-- Positions List -->
<div class="glass-card p-2" style="background: #161a1e; border: 1px solid #2b3139;">
<div class="fw-bold mb-2 small border-bottom border-secondary pb-1 d-flex justify-content-between">
<span>当前持仓/挂单</span>
<span class="text-warning x-small" style="font-weight: normal; cursor: pointer;" onclick="location.reload()"><i class="bi bi-arrow-clockwise"></i> 刷新</span>
</div>
<div id="m-positions-list">
{% if trade_type == 'CONTRACT' %}
{% for p in account.positions.all %}
{% if p.is_active %}
<div class="mb-2 p-2 bg-dark rounded border-start border-3 {% if p.side == 'LONG' %}border-success{% else %}border-danger{% endif %}" style="border: 1px solid #2b3139;">
<div class="d-flex justify-content-between x-small mb-1">
<span class="fw-bold text-white">{{ p.symbol }} <span class="badge bg-secondary">{{ p.leverage }}x</span> <span class="{% if p.side == 'LONG' %}text-success{% else %}text-danger{% endif %}">{% if p.side == 'LONG' %}多{% else %}空{% endif %}</span></span>
<span class="upl-val fw-bold" data-entry="{{ p.entry_price }}" data-side="{{ p.side }}" data-lots="{{ p.lots }}">--</span>
</div>
<div class="d-flex justify-content-between x-small text-secondary" style="font-size: 10px;">
<span>手数: {{ p.lots }}</span>
<span>开仓价: {{ p.entry_price|floatformat:2 }}</span>
</div>
<div class="d-flex justify-content-between x-small text-secondary mb-1" style="font-size: 10px;">
<span>保证金: {{ p.margin|floatformat:2 }}</span>
<span class="current-p-val" data-symbol="{{ p.symbol }}">当前: --</span>
</div>
<button class="btn btn-outline-danger btn-xs py-0 w-100 mt-1" style="font-size: 11px;" onclick="closePos({{ p.id }})">市价平仓</button>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% for o in account.orders.all %}
{% if o.status == 'PENDING' %}
<div class="mb-2 p-2 bg-dark rounded border-start border-3 border-warning" style="border: 1px solid #2b3139;">
<div class="d-flex justify-content-between x-small">
<span class="text-white">{{ o.symbol }} ({{ o.get_trade_type_display }}) <span class="{% if o.side == 'BUY' %}text-success{% else %}text-danger{% endif %}">{{ o.get_side_display }}</span></span>
<span class="text-warning">委托中</span>
</div>
<div class="d-flex justify-content-between x-small text-secondary mt-1" style="font-size: 10px;">
<span>价格: {{ o.price|default:"市价" }}</span>
<span>数量: {{ o.amount }}</span>
</div>
<button class="btn btn-outline-secondary btn-xs py-0 w-100 mt-1" style="font-size: 11px;">撤单</button>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<style>
.btn-xs { padding: 1px 5px; font-size: 10px; }
.order-row { display: flex; justify-content: space-between; margin-bottom: 2px; height: 16px; overflow: hidden; position: relative; }
.order-row span { position: relative; z-index: 1; }
.ask-bg { position: absolute; right: 0; top: 0; bottom: 0; background: rgba(246, 70, 93, 0.1); }
.bid-bg { position: absolute; right: 0; top: 0; bottom: 0; background: rgba(14, 203, 129, 0.1); }
.custom-range::-webkit-slider-thumb { background: #f0b90b; }
</style>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="https://s3.tradingview.com/tv.js"></script>
<script>
const symbol = '{{ symbol }}';
const tradeType = '{{ trade_type }}';
const baseAsset = '{{ base_symbol }}';
const balance = parseFloat("{{ account.balance|default:0 }}");
const assetBalance = parseFloat("{{ base_asset_balance|default:0 }}");
let currentPrice = 0;
let leverage = 20;
let side = 'BUY';
function initTV() {
new TradingView.widget({
"width": "100%", "height": "100%", "symbol": "BINANCE:" + symbol,
"interval": "15", "theme": "dark", "style": "1", "locale": "zh_CN",
"container_id": "tradingview_widget", "hide_side_toolbar": true, "save_image": false
});
}
async function getMarkets() {
try {
const r = await fetch('/api/market_data/');
const data = await r.json();
const list = document.getElementById('mobile-coin-list');
list.innerHTML = '';
data.forEach(c => {
const b = c.symbol.replace('USDT', '');
const iconUrl = `https://static.okx.com/cdn/oksupport/asset/currency/icon/${b.toLowerCase()}.png`;
list.innerHTML += `<li><a class="dropdown-item d-flex justify-content-between" href="?symbol=${c.symbol}"><span><img src="${iconUrl}" style="width:16px;margin-right:5px;" onerror="this.src='https://static.okx.com/cdn/oksupport/asset/currency/icon/generic.png'">${b}</span><span class="${c.change>=0?'text-success':'text-danger'}">${c.price}</span></a></li>`;
});
} catch(e) {}
}
function toggleSide(s) {
side = s;
const btn = document.getElementById('m-submit-btn');
if (btn) {
if (s === 'BUY') {
btn.className = 'btn btn-success w-100 fw-bold btn-sm';
btn.textContent = '立即买入';
} else {
btn.className = 'btn btn-danger w-100 fw-bold btn-sm';
btn.textContent = '立即卖出';
}
}
const avail = document.getElementById('m-available');
if (tradeType === 'SPOT') {
if (s === 'BUY') avail.textContent = balance.toFixed(2) + ' USDT';
else avail.textContent = assetBalance.toFixed(4) + ' ' + baseAsset;
} else {
avail.textContent = balance.toFixed(2) + ' USDT';
}
toggleModeMobile();
}
function toggleModeMobile() {
const mode = tradeType === 'SPOT'
? document.getElementById('spot-mode-select').value
: document.getElementById('c-mode-select').value;
const pInput = document.getElementById('m-price');
const unit = document.getElementById('m-unit');
const label = document.getElementById('m-label');
if (mode === 'market') {
pInput.value = currentPrice || '市价';
pInput.disabled = true;
pInput.style.color = '#f0b90b';
if (tradeType === 'SPOT' && side === 'BUY') {
label.textContent = '额';
unit.textContent = 'USDT';
} else {
label.textContent = '数';
unit.textContent = tradeType === 'SPOT' ? baseAsset : '张';
}
} else {
pInput.value = '';
pInput.disabled = false;
pInput.style.color = 'white';
label.textContent = '数';
unit.textContent = tradeType === 'SPOT' ? baseAsset : '张';
}
applySliderMobile();
}
function applySliderMobile() {
const val = document.getElementById('m-slider').value;
const mode = tradeType === 'SPOT'
? document.getElementById('spot-mode-select').value
: document.getElementById('c-mode-select').value;
const amountInput = document.getElementById('m-amount');
if (tradeType === 'SPOT') {
if (side === 'BUY') {
if (mode === 'market') {
amountInput.value = (balance * (val / 100)).toFixed(2);
} else {
const price = parseFloat(document.getElementById('m-price').value) || currentPrice;
if (price > 0) amountInput.value = (balance * (val / 100) / price).toFixed(4);
else amountInput.value = 0;
}
} else {
amountInput.value = (assetBalance * (val / 100)).toFixed(4);
}
} else {
const maxLots = (balance * leverage) / 100;
amountInput.value = Math.floor(maxLots * (val / 100));
}
}
function updateSliderMobile() {
const val = parseFloat(document.getElementById('m-amount').value) || 0;
const slider = document.getElementById('m-slider');
if (tradeType === 'SPOT') {
if (side === 'BUY') {
const mode = document.getElementById('spot-mode-select').value;
if (mode === 'market') {
if (balance > 0) slider.value = (val / balance) * 100;
} else {
const price = parseFloat(document.getElementById('m-price').value) || currentPrice;
if (balance > 0 && price > 0) slider.value = (val * price / balance) * 100;
}
} else {
if (assetBalance > 0) slider.value = (val / assetBalance) * 100;
}
} else {
const maxLots = (balance * leverage) / 100;
if (maxLots > 0) slider.value = (val / maxLots) * 100;
}
}
function setLev(l) {
leverage = l;
document.getElementById('lev-btn').textContent = l + 'x';
applySliderMobile();
}
async function tick() {
try {
const r = await fetch('/api/market_data/');
const data = await r.json();
const d = data.find(c => c.symbol === symbol);
if (!d) return;
currentPrice = parseFloat(d.price);
document.getElementById('m-current-price').textContent = currentPrice.toLocaleString();
const pInput = document.getElementById('m-price');
if (pInput && pInput.disabled) pInput.value = currentPrice;
const askD = document.getElementById('m-asks'); const bidD = document.getElementById('m-bids');
askD.innerHTML = ''; bidD.innerHTML = '';
for (let i = 0; i < 5; i++) {
const ap = (currentPrice+(5-i)*0.1).toFixed(2);
const bp = (currentPrice-(i+1)*0.1).toFixed(2);
askD.innerHTML = `<div class="order-row"><span class="text-danger">${ap}</span><span class="text-secondary">${(Math.random()*0.5).toFixed(3)}</span><div class="ask-bg" style="width:${Math.random()*60}%"></div></div>` + askD.innerHTML;
bidD.innerHTML += `<div class="order-row"><span class="text-success">${bp}</span><span class="text-secondary">${(Math.random()*0.5).toFixed(3)}</span><div class="bid-bg" style="width:${Math.random()*60}%"></div></div>`;
}
document.querySelectorAll('.current-p-val').forEach(el => { el.textContent = '当前: ' + currentPrice.toLocaleString(); });
document.querySelectorAll('.upl-val').forEach(el => {
const entry = parseFloat(el.getAttribute('data-entry'));
const sideP = el.getAttribute('data-side');
const lots = parseFloat(el.getAttribute('data-lots'));
let upl = sideP === 'LONG' ? (currentPrice - entry) / entry * (lots * 100) : (entry - currentPrice) / entry * (lots * 100);
el.textContent = (upl >= 0 ? '+' : '') + upl.toFixed(2) + ' USDT';
el.className = 'upl-val fw-bold ' + (upl >= 0 ? 'text-success' : 'text-danger');
});
} catch(e) {}
}
async function submitOrderMobile(sideParam) {
const mode = tradeType === 'SPOT' ? document.getElementById('spot-mode-select').value : document.getElementById('c-mode-select').value;
const price = document.getElementById('m-price').value;
const amount = document.getElementById('m-amount').value;
if (mode === 'limit' && (!price || isNaN(parseFloat(price)))) {
alert('请输入有效委托价格'); return;
}
if (!amount || amount <= 0) {
alert('请输入数量'); return;
}
try {
const r = await fetch('{% url "core:submit_order" %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
body: JSON.stringify({
symbol: symbol, side: sideParam, trade_type: tradeType, order_type: mode.toUpperCase(),
price: mode === 'market' ? null : price, amount: amount, leverage: leverage
})
});
const res = await r.json();
if (res.status === 'success') { alert('下单成功!'); location.reload(); }
else { alert('失败: ' + res.message); }
} catch(e) { alert('提交出错'); }
}
async function closePos(id) {
if (confirm('确定平仓?')) {
const r = await fetch(`/api/close_position/${id}/`, { method: 'POST', headers: { 'X-CSRFToken': '{{ csrf_token }}' } });
const res = await r.json();
if (res.status === 'success') location.reload();
}
}
initTV(); getMarkets(); tick();
setInterval(tick, 2000);
toggleSide('BUY');
</script>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends 'core/mobile/base_mobile.html' %}
{% block content %}
<div class="p-3">
<div class="mb-4">
<a href="{% url 'core:profile' %}" class="text-decoration-none text-secondary small"><i class="bi bi-chevron-left"></i> 返回</a>
<h4 class="fw-bold mt-2">实名认证</h4>
</div>
<div class="glass-card p-4 text-center">
{% if account.kyc_status == 'UNVERIFIED' %}
<i class="bi bi-shield-lock fs-1 text-secondary mb-3"></i>
<h5 class="fw-bold">未认证</h5>
<p class="text-secondary small mb-4">完成实名认证以提高账户额度和安全性</p>
<form method="POST">
{% csrf_token %}
<div class="mb-3 text-start">
<label class="form-label small text-secondary">真实姓名</label>
<input type="text" class="form-control" required placeholder="请输入您的真实姓名">
</div>
<div class="mb-4 text-start">
<label class="form-label small text-secondary">身份证号 / 护照号</label>
<input type="text" class="form-control" required placeholder="请输入证件号码">
</div>
<button type="submit" class="btn btn-warning w-100 fw-bold">提交审核</button>
</form>
{% elif account.kyc_status == 'PENDING' %}
<i class="bi bi-clock-history fs-1 text-warning mb-3"></i>
<h5 class="fw-bold">审核中</h5>
<p class="text-secondary small">您的认证申请正在处理中,请耐心等待。</p>
{% elif account.kyc_status == 'VERIFIED' %}
<i class="bi bi-patch-check-fill fs-1 text-success mb-3"></i>
<h5 class="fw-bold">已认证</h5>
<p class="text-secondary small">您的账户已通过实名认证。</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends 'core/mobile/base_mobile.html' %}
{% block content %}
<div class="p-3">
<div class="mb-4">
<a href="{% url 'core:profile' %}" class="text-decoration-none text-secondary small"><i class="bi bi-chevron-left"></i> 返回</a>
<h4 class="fw-bold mt-2">提现 USDT</h4>
</div>
<div class="glass-card p-3 mb-3">
<div class="d-flex justify-content-between x-small mb-1">
<span class="text-secondary">可用余额</span>
<span class="fw-bold">{{ account.balance }} USDT</span>
</div>
</div>
<form method="POST" class="glass-card p-3">
{% csrf_token %}
<div class="mb-3">
<label class="form-label small text-secondary">提币网络</label>
<select class="form-select form-select-sm bg-dark text-white border-secondary">
<option>TRC20</option>
<option>ERC20</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small text-secondary">提币地址</label>
<input type="text" name="address" class="form-control" placeholder="请输入收币地址" required>
</div>
<div class="mb-3">
<label class="form-label small text-secondary">提币数量</label>
<div class="input-group">
<input type="number" name="amount" class="form-control" placeholder="最小提币 10" required>
<button type="button" class="btn btn-outline-warning x-small" onclick="document.getElementsByName('amount')[0].value={{ account.balance }}">全部</button>
</div>
<div class="x-small text-secondary mt-1">手续费: 1 USDT</div>
</div>
<button type="submit" class="btn btn-warning w-100 fw-bold">立即提现</button>
</form>
</div>
{% endblock %}

View File

@ -4,7 +4,7 @@
{% block content %} {% block content %}
<div class="container-fluid px-2 py-2" style="background-color: #0b0e11; min-height: 90vh;"> <div class="container-fluid px-2 py-2" style="background-color: #0b0e11; min-height: 90vh;">
<div class="row g-2"> <div class="row g-2">
<!-- Left: Coin List Sidebar --> <!-- Left: Coin List Sidebar (Desktop Only) -->
<div class="col-lg-2 d-none d-lg-block"> <div class="col-lg-2 d-none d-lg-block">
<div class="glass-card h-100 p-2" style="border-radius: 4px; border: 1px solid #2b3139; background: #161a1e;"> <div class="glass-card h-100 p-2" style="border-radius: 4px; border: 1px solid #2b3139; background: #161a1e;">
<div class="p-2 mb-2"> <div class="p-2 mb-2">
@ -23,7 +23,6 @@
</tr> </tr>
</thead> </thead>
<tbody id="left-coin-list"> <tbody id="left-coin-list">
<!-- Populated by JS -->
</tbody> </tbody>
</table> </table>
</div> </div>
@ -40,7 +39,6 @@
<span id="current-symbol-display">{{ symbol }}</span> <i class="bi bi-caret-down-fill small"></i> <span id="current-symbol-display">{{ symbol }}</span> <i class="bi bi-caret-down-fill small"></i>
</button> </button>
<ul class="dropdown-menu dropdown-menu-dark shadow-lg" id="mobile-coin-list" style="max-height: 400px; overflow-y: auto; width: 250px;"> <ul class="dropdown-menu dropdown-menu-dark shadow-lg" id="mobile-coin-list" style="max-height: 400px; overflow-y: auto; width: 250px;">
<!-- Populated by JS -->
</ul> </ul>
</div> </div>
<div class="me-4"> <div class="me-4">
@ -82,7 +80,7 @@
<div class="input-group input-group-sm mb-3"> <div class="input-group input-group-sm mb-3">
<span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;">价格</span> <span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;">价格</span>
<input type="text" id="buy-price" class="form-control bg-transparent text-white border-secondary" placeholder="0.00"> <input type="text" id="buy-price" class="form-control bg-transparent text-white border-secondary" placeholder="请输入价格">
<span class="input-group-text bg-dark text-secondary border-secondary">USDT</span> <span class="input-group-text bg-dark text-secondary border-secondary">USDT</span>
</div> </div>
@ -117,7 +115,7 @@
<div class="input-group input-group-sm mb-3"> <div class="input-group input-group-sm mb-3">
<span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;">价格</span> <span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;">价格</span>
<input type="text" id="sell-price" class="form-control bg-transparent text-white border-secondary" placeholder="0.00"> <input type="text" id="sell-price" class="form-control bg-transparent text-white border-secondary" placeholder="请输入价格">
<span class="input-group-text bg-dark text-secondary border-secondary">USDT</span> <span class="input-group-text bg-dark text-secondary border-secondary">USDT</span>
</div> </div>
@ -164,7 +162,7 @@
<div class="col-md-6 border-end border-secondary"> <div class="col-md-6 border-end border-secondary">
<div class="input-group input-group-sm mb-3"> <div class="input-group input-group-sm mb-3">
<span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;">价格</span> <span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;">价格</span>
<input type="text" id="c-price" class="form-control bg-transparent text-white border-secondary" placeholder="0.00"> <input type="text" id="c-price" class="form-control bg-transparent text-white border-secondary" placeholder="请输入价格">
<span class="input-group-text bg-dark text-secondary border-secondary">USDT</span> <span class="input-group-text bg-dark text-secondary border-secondary">USDT</span>
</div> </div>
<div class="input-group input-group-sm mb-3"> <div class="input-group input-group-sm mb-3">
@ -261,8 +259,8 @@
</div> </div>
</div> </div>
<!-- Right: Order Book --> <!-- Right: Order Book (Desktop Only) -->
<div class="col-lg-3"> <div class="col-lg-3 d-none d-lg-block">
<div class="glass-card h-100 p-2" style="border-radius: 4px; border: 1px solid #2b3139; background: #161a1e;"> <div class="glass-card h-100 p-2" style="border-radius: 4px; border: 1px solid #2b3139; background: #161a1e;">
<div class="d-flex justify-content-between text-secondary x-small mb-2 px-2"> <div class="d-flex justify-content-between text-secondary x-small mb-2 px-2">
<span>价格(USDT)</span> <span>价格(USDT)</span>
@ -316,9 +314,9 @@
async function getMarkets() { async function getMarkets() {
try { try {
const r = await fetch('https://api.binance.com/api/v3/ticker/24hr'); const r = await fetch('/api/market_data/');
const data = await r.json(); const data = await r.json();
allCoins = data.filter(c => c.symbol.endsWith('USDT')).slice(0, 100); allCoins = data;
renderCoins(); renderCoins();
} catch(e) {} } catch(e) {}
} }
@ -326,21 +324,22 @@
function renderCoins(filter = '') { function renderCoins(filter = '') {
const list = document.getElementById('left-coin-list'); const list = document.getElementById('left-coin-list');
const mobileList = document.getElementById('mobile-coin-list'); const mobileList = document.getElementById('mobile-coin-list');
list.innerHTML = ''; mobileList.innerHTML = ''; if (list) list.innerHTML = '';
if (mobileList) mobileList.innerHTML = '';
allCoins.forEach(c => { allCoins.forEach(c => {
if (filter && !c.symbol.toLowerCase().includes(filter.toLowerCase())) return; if (filter && !c.symbol.toLowerCase().includes(filter.toLowerCase())) return;
const base = c.symbol.replace('USDT', ''); const base = c.symbol.replace('USDT', '');
const chg = parseFloat(c.priceChangePercent); const chg = parseFloat(c.change);
const iconUrl = `https://static.okx.com/cdn/oksupport/asset/currency/icon/${base.toLowerCase()}.png`; const iconUrl = `https://static.okx.com/cdn/oksupport/asset/currency/icon/${base.toLowerCase()}.png`;
const row = ` const row = `
<tr style="cursor: pointer;" onclick="location.href='?symbol=${c.symbol}'"> <tr style="cursor: pointer;" onclick="location.href='?symbol=${c.symbol}'">
<td><img src="${iconUrl}" class="coin-icon-small" onerror="this.src='https://static.okx.com/cdn/oksupport/asset/currency/icon/generic.png'">${base}</td> <td><img src="${iconUrl}" class="coin-icon-small" onerror="this.src='https://static.okx.com/cdn/oksupport/asset/currency/icon/generic.png'">${base}</td>
<td class="text-end fw-bold">${parseFloat(c.lastPrice).toLocaleString()}</td> <td class="text-end fw-bold">${parseFloat(c.price).toLocaleString()}</td>
<td class="text-end ${chg>=0?'text-success':'text-danger'}">${chg>=0?'+':''}${chg.toFixed(2)}%</td> <td class="text-end ${chg>=0?'text-success':'text-danger'}">${chg>=0?'+':''}${chg.toFixed(2)}%</td>
</tr>`; </tr>`;
list.innerHTML += row; if (list) list.innerHTML += row;
mobileList.innerHTML += `<li><a class="dropdown-item d-flex justify-content-between" href="?symbol=${c.symbol}"><span><img src="${iconUrl}" class="coin-icon-small" onerror="this.src='https://static.okx.com/cdn/oksupport/asset/currency/icon/generic.png'">${base}</span><span class="${chg>=0?'text-success':'text-danger'}">${parseFloat(c.lastPrice).toLocaleString()}</span></a></li>`; if (mobileList) mobileList.innerHTML += `<li><a class="dropdown-item d-flex justify-content-between" href="?symbol=${c.symbol}"><span><img src="${iconUrl}" class="coin-icon-small" onerror="this.src='https://static.okx.com/cdn/oksupport/asset/currency/icon/generic.png'">${base}</span><span class="${chg>=0?'text-success':'text-danger'}">${parseFloat(c.price).toLocaleString()}</span></a></li>`;
}); });
} }
@ -349,42 +348,43 @@
} }
function toggleMode(side, mode) { function toggleMode(side, mode) {
let pInput;
if (side === 'buy') { if (side === 'buy') {
const pInput = document.getElementById('buy-price'); pInput = document.getElementById('buy-price');
const label = document.getElementById('buy-label'); const label = document.getElementById('buy-label');
const unit = document.getElementById('buy-unit'); const unit = document.getElementById('buy-unit');
if (mode === 'market') { if (mode === 'market') {
pInput.value = '市价价格'; pInput.value = currentPrice || '市价';
pInput.disabled = true; pInput.disabled = true;
pInput.style.color = '#f0b90b'; pInput.style.color = '#f0b90b';
label.textContent = '成交额'; label.textContent = '成交额';
unit.textContent = 'USDT'; unit.textContent = 'USDT';
} else { } else {
pInput.value = currentPrice || ''; pInput.value = '';
pInput.disabled = false; pInput.disabled = false;
pInput.style.color = 'white'; pInput.style.color = 'white';
label.textContent = '数量'; label.textContent = '数量';
unit.textContent = baseAsset; unit.textContent = baseAsset;
} }
} else if (side === 'sell') { } else if (side === 'sell') {
const pInput = document.getElementById('sell-price'); pInput = document.getElementById('sell-price');
if (mode === 'market') { if (mode === 'market') {
pInput.value = '市价价格'; pInput.value = currentPrice || '市价';
pInput.disabled = true; pInput.disabled = true;
pInput.style.color = '#f0b90b'; pInput.style.color = '#f0b90b';
} else { } else {
pInput.value = currentPrice || ''; pInput.value = '';
pInput.disabled = false; pInput.disabled = false;
pInput.style.color = 'white'; pInput.style.color = 'white';
} }
} else if (side === 'contract') { } else if (side === 'contract') {
const pInput = document.getElementById('c-price'); pInput = document.getElementById('c-price');
if (mode === 'market') { if (mode === 'market') {
pInput.value = '市价价格'; pInput.value = currentPrice || '市价';
pInput.disabled = true; pInput.disabled = true;
pInput.style.color = '#f0b90b'; pInput.style.color = '#f0b90b';
} else { } else {
pInput.value = currentPrice || ''; pInput.value = '';
pInput.disabled = false; pInput.disabled = false;
pInput.style.color = 'white'; pInput.style.color = 'white';
} }
@ -398,15 +398,18 @@
if (mode === 'market') { if (mode === 'market') {
document.getElementById('buy-amount').value = (balance * (val / 100)).toFixed(2); document.getElementById('buy-amount').value = (balance * (val / 100)).toFixed(2);
} else { } else {
const price = parseFloat(document.getElementById('buy-price').value); const price = parseFloat(document.getElementById('buy-price').value) || currentPrice;
if (price > 0) document.getElementById('buy-amount').value = (balance * (val / 100) / price).toFixed(4); if (price > 0) document.getElementById('buy-amount').value = (balance * (val / 100) / price).toFixed(4);
else document.getElementById('buy-amount').value = 0;
} }
} else if (side === 'sell') { } else if (side === 'sell') {
document.getElementById('sell-amount').value = (assetBalance * (val / 100)).toFixed(4); document.getElementById('sell-amount').value = (assetBalance * (val / 100)).toFixed(4);
} else if (side === 'contract') { } else if (side === 'contract') {
const margin = (balance * (val / 100)); const maxLots = (balance * leverage) / 100;
const lots = Math.floor(maxLots * (val / 100));
document.getElementById('c-amount').value = lots;
const margin = (100 * lots) / leverage;
document.getElementById('c-margin-display').textContent = margin.toFixed(2); document.getElementById('c-margin-display').textContent = margin.toFixed(2);
document.getElementById('c-amount').value = Math.floor((margin * leverage) / 100);
} }
} }
@ -418,18 +421,19 @@
if (mode === 'market') { if (mode === 'market') {
percent = (amount / balance) * 100; percent = (amount / balance) * 100;
} else { } else {
const price = parseFloat(document.getElementById('buy-price').value) || 0; const price = parseFloat(document.getElementById('buy-price').value) || currentPrice;
percent = (amount * price / balance) * 100; if (price > 0) percent = (amount * price / balance) * 100;
} }
document.getElementById('buy-slider').value = Math.min(100, percent); document.getElementById('buy-slider').value = Math.min(100, percent || 0);
} else if (side === 'sell') { } else if (side === 'sell') {
const amount = parseFloat(document.getElementById('sell-amount').value) || 0; const amount = parseFloat(document.getElementById('sell-amount').value) || 0;
document.getElementById('sell-slider').value = Math.min(100, (amount / assetBalance) * 100); if (assetBalance > 0) document.getElementById('sell-slider').value = Math.min(100, (amount / assetBalance) * 100);
} else if (side === 'contract') { } else if (side === 'contract') {
const lots = parseFloat(document.getElementById('c-amount').value) || 0; const lots = parseFloat(document.getElementById('c-amount').value) || 0;
const maxLots = (balance * leverage) / 100;
if (maxLots > 0) document.getElementById('c-slider').value = Math.min(100, (lots / maxLots) * 100);
const margin = (100 * lots) / leverage; const margin = (100 * lots) / leverage;
document.getElementById('c-margin-display').textContent = margin.toFixed(2); document.getElementById('c-margin-display').textContent = margin.toFixed(2);
document.getElementById('c-slider').value = Math.min(100, (margin / balance) * 100);
} }
} }
@ -442,31 +446,35 @@
async function tick() { async function tick() {
try { try {
const r = await fetch('https://api.binance.com/api/v3/ticker/24hr?symbol=' + symbol); const r = await fetch('/api/market_data/');
const d = await r.json(); const data = await r.json();
currentPrice = parseFloat(d.lastPrice); const d = data.find(c => c.symbol === symbol);
if (!d) return;
currentPrice = parseFloat(d.price);
document.getElementById('header-price').textContent = currentPrice.toLocaleString(); document.getElementById('header-price').textContent = currentPrice.toLocaleString();
document.getElementById('header-price-usd').textContent = currentPrice.toLocaleString(); document.getElementById('header-price-usd').textContent = currentPrice.toLocaleString();
document.getElementById('current-price-book').textContent = currentPrice.toLocaleString(); const cpBook = document.getElementById('current-price-book');
if (cpBook) cpBook.textContent = currentPrice.toLocaleString();
const chg = parseFloat(d.priceChangePercent); // Update market price in box ONLY if disabled (Market Order)
['buy-price', 'sell-price', 'c-price'].forEach(id => {
const el = document.getElementById(id);
if (el && el.disabled) el.value = currentPrice;
});
const chg = parseFloat(d.change);
document.getElementById('header-change').textContent = (chg>=0?'+':'') + chg.toFixed(2) + '%'; document.getElementById('header-change').textContent = (chg>=0?'+':'') + chg.toFixed(2) + '%';
document.getElementById('header-change').className = 'fw-bold ' + (chg>=0?'text-success':'text-danger'); document.getElementById('header-change').className = 'fw-bold ' + (chg>=0?'text-success':'text-danger');
document.getElementById('header-volume').textContent = (parseFloat(d.quoteVolume)/1000000).toFixed(2) + 'M';
const askD = document.getElementById('asks'); const bidD = document.getElementById('bids'); const askD = document.getElementById('asks'); const bidD = document.getElementById('bids');
askD.innerHTML = ''; bidD.innerHTML = ''; if (askD && bidD) {
for (let i = 0; i < 10; i++) { askD.innerHTML = ''; bidD.innerHTML = '';
askD.innerHTML = `<div class="order-book-row"><span class="text-danger">${(currentPrice+(10-i)*0.1).toFixed(2)}</span><span>${(Math.random()*1.2).toFixed(4)}</span><div class="ask-bg" style="width:${Math.random()*70}%"></div></div>` + askD.innerHTML; for (let i = 0; i < 10; i++) {
bidD.innerHTML += `<div class="order-book-row"><span class="text-success">${(currentPrice-(i+1)*0.1).toFixed(2)}</span><span>${(Math.random()*1.2).toFixed(4)}</span><div class="bid-bg" style="width:${Math.random()*70}%"></div></div>`; askD.innerHTML = `<div class="order-book-row"><span class="text-danger">${(currentPrice+(10-i)*0.1).toFixed(2)}</span><span>${(Math.random()*1.2).toFixed(4)}</span><div class="ask-bg" style="width:${Math.random()*70}%"></div></div>` + askD.innerHTML;
} bidD.innerHTML += `<div class="order-book-row"><span class="text-success">${(currentPrice-(i+1)*0.1).toFixed(2)}</span><span>${(Math.random()*1.2).toFixed(4)}</span><div class="bid-bg" style="width:${Math.random()*70}%"></div></div>`;
}
if (tradeType === 'SPOT') {
if (!document.getElementById('buy-price').value && !document.getElementById('buy-price').disabled) document.getElementById('buy-price').value = currentPrice;
if (!document.getElementById('sell-price').value && !document.getElementById('sell-price').disabled) document.getElementById('sell-price').value = currentPrice;
} else {
if (!document.getElementById('c-price').value && !document.getElementById('c-price').disabled) document.getElementById('c-price').value = currentPrice;
} }
document.querySelectorAll('.current-p-val').forEach(el => { el.textContent = currentPrice.toLocaleString(); }); document.querySelectorAll('.current-p-val').forEach(el => { el.textContent = currentPrice.toLocaleString(); });
@ -500,13 +508,22 @@
amount = document.getElementById('c-amount').value; amount = document.getElementById('c-amount').value;
} }
if (mode === 'LIMIT' && !price) {
alert('请输入委托价格');
return;
}
if (!amount || amount <= 0) {
alert('请输入有效数量/手数');
return;
}
try { try {
const r = await fetch('{% url "core:submit_order" %}', { const r = await fetch('{% url "core:submit_order" %}', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' }, headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
body: JSON.stringify({ body: JSON.stringify({
symbol: symbol, side: side, trade_type: tradeType, order_type: mode, symbol: symbol, side: side, trade_type: tradeType, order_type: mode,
price: price === '市价价格' ? null : price, amount: amount, leverage: leverage price: mode === 'MARKET' ? null : price, amount: amount, leverage: leverage
}) })
}); });
const res = await r.json(); const res = await r.json();

View File

@ -12,7 +12,14 @@ import json
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.db import transaction from django.db import transaction
def is_mobile(request):
user_agent = request.META.get('HTTP_USER_AGENT', '').lower()
mobile_indicators = ['iphone', 'android', 'phone', 'mobile']
return any(indicator in user_agent for indicator in mobile_indicators)
def index(request): def index(request):
if is_mobile(request):
return render(request, 'core/mobile/index.html')
return render(request, 'core/index.html') return render(request, 'core/index.html')
def trade(request, trade_type='spot'): def trade(request, trade_type='spot'):
@ -39,11 +46,14 @@ def trade(request, trade_type='spot'):
'assets': assets, 'assets': assets,
'base_asset_balance': base_asset_balance, 'base_asset_balance': base_asset_balance,
} }
return render(request, 'core/trade.html', context)
template = 'core/mobile/trade.html' if is_mobile(request) else 'core/trade.html'
return render(request, template, context)
def market_center(request): def market_center(request):
cryptos = Cryptocurrency.objects.filter(is_active=True) cryptos = Cryptocurrency.objects.filter(is_active=True)
return render(request, 'core/market_center.html', {'cryptos': cryptos}) template = 'core/mobile/market_center.html' if is_mobile(request) else 'core/market_center.html'
return render(request, template, {'cryptos': cryptos})
def placeholder_view(request, title): def placeholder_view(request, title):
settings = SiteSettings.objects.first() settings = SiteSettings.objects.first()
@ -85,7 +95,8 @@ def profile(request):
'recent_orders': recent_orders, 'recent_orders': recent_orders,
'positions': positions, 'positions': positions,
} }
return render(request, 'core/profile.html', context) template = 'core/mobile/profile.html' if is_mobile(request) else 'core/profile.html'
return render(request, template, context)
@login_required @login_required
def deposit(request): def deposit(request):
@ -102,7 +113,8 @@ def deposit(request):
tx_hash=tx_id tx_hash=tx_id
) )
return redirect('core:profile') return redirect('core:profile')
return render(request, 'core/deposit.html') template = 'core/mobile/deposit.html' if is_mobile(request) else 'core/deposit.html'
return render(request, template)
@login_required @login_required
def withdraw(request): def withdraw(request):
@ -123,7 +135,8 @@ def withdraw(request):
tx_hash=f"wd_{random.randint(1000, 9999)}" tx_hash=f"wd_{random.randint(1000, 9999)}"
) )
return redirect('core:profile') return redirect('core:profile')
return render(request, 'core/withdraw.html', {'account': account}) template = 'core/mobile/withdraw.html' if is_mobile(request) else 'core/withdraw.html'
return render(request, template, {'account': account})
@login_required @login_required
def verify(request): def verify(request):
@ -132,7 +145,8 @@ def verify(request):
account.kyc_status = 'pending' account.kyc_status = 'pending'
account.save() account.save()
return redirect('core:profile') return redirect('core:profile')
return render(request, 'core/verify.html', {'account': account}) template = 'core/mobile/verify.html' if is_mobile(request) else 'core/verify.html'
return render(request, template, {'account': account})
def register_view(request): def register_view(request):
if request.method == 'POST': if request.method == 'POST':
@ -157,7 +171,8 @@ def register_view(request):
captcha_text, captcha_result = generate_captcha() captcha_text, captcha_result = generate_captcha()
request.session['captcha_result'] = captcha_result request.session['captcha_result'] = captcha_result
return render(request, 'core/register.html', { template = 'core/mobile/register.html' if is_mobile(request) else 'core/register.html'
return render(request, template, {
'captcha_text': captcha_text 'captcha_text': captcha_text
}) })
@ -179,7 +194,8 @@ def login_view(request):
return redirect('core:index') return redirect('core:index')
else: else:
form = AuthenticationForm() form = AuthenticationForm()
return render(request, 'core/login.html', {'form': form}) template = 'core/mobile/login.html' if is_mobile(request) else 'core/login.html'
return render(request, template, {'form': form})
@login_required @login_required
@csrf_exempt @csrf_exempt
@ -200,9 +216,13 @@ def submit_order(request):
account = request.user.account account = request.user.account
base_symbol = symbol.replace('USDT', '') base_symbol = symbol.replace('USDT', '')
# Get current market price for validations and market orders # Get price (consider manual override)
crypto = Cryptocurrency.objects.filter(symbol=base_symbol).first() crypto = Cryptocurrency.objects.filter(symbol=base_symbol).first()
settings = SiteSettings.objects.first()
current_price = crypto.current_price if (crypto and crypto.current_price > 0) else decimal.Decimal('48000') current_price = crypto.current_price if (crypto and crypto.current_price > 0) else decimal.Decimal('48000')
if settings and settings.is_pinning_active and crypto and crypto.manual_price:
current_price = crypto.manual_price
with transaction.atomic(): with transaction.atomic():
if trade_type == 'SPOT': if trade_type == 'SPOT':
@ -325,17 +345,36 @@ def close_position(request, position_id):
base_symbol = position.symbol.replace('USDT', '') base_symbol = position.symbol.replace('USDT', '')
crypto = Cryptocurrency.objects.filter(symbol=base_symbol).first() crypto = Cryptocurrency.objects.filter(symbol=base_symbol).first()
settings = SiteSettings.objects.first()
current_price = crypto.current_price if (crypto and crypto.current_price > 0) else decimal.Decimal('48000') current_price = crypto.current_price if (crypto and crypto.current_price > 0) else decimal.Decimal('48000')
if settings and settings.is_pinning_active and crypto and crypto.manual_price:
current_price = crypto.manual_price
account = request.user.account
face_value = decimal.Decimal('100') face_value = decimal.Decimal('100')
# Win/Loss Control Logic
# 100 = Must Win, -100 = Must Loss
forced_upl = None
if account.win_loss_control != 0:
# Force a small profit or loss
total_value = position.lots * face_value
if account.win_loss_control > 0: # Force Profit
forced_upl = total_value * decimal.Decimal('0.05') # 5% profit
else: # Force Loss
forced_upl = -total_value * decimal.Decimal('0.05') # 5% loss
# Calculate Unrealized P&L # Calculate Unrealized P&L
if position.side == 'LONG': if position.side == 'LONG':
upl = (current_price - position.entry_price) / position.entry_price * (position.lots * face_value) upl = (current_price - position.entry_price) / position.entry_price * (position.lots * face_value)
else: else:
upl = (position.entry_price - current_price) / position.entry_price * (position.lots * face_value) upl = (position.entry_price - current_price) / position.entry_price * (position.lots * face_value)
# If control is active, override upl
if forced_upl is not None:
upl = forced_upl
with transaction.atomic(): with transaction.atomic():
account = request.user.account
# Settlement: Margin + P&L - Fee (0.05%) # Settlement: Margin + P&L - Fee (0.05%)
fee = (position.lots * face_value) * decimal.Decimal('0.0005') fee = (position.lots * face_value) * decimal.Decimal('0.0005')
account.balance += (position.margin + upl - fee) account.balance += (position.margin + upl - fee)
@ -353,12 +392,19 @@ def close_position(request, position_id):
return JsonResponse({'status': 'success'}) return JsonResponse({'status': 'success'})
def market_data(request): def market_data(request):
settings = SiteSettings.objects.first()
cryptos = Cryptocurrency.objects.filter(is_active=True) cryptos = Cryptocurrency.objects.filter(is_active=True)
data = [] data = []
for c in cryptos: for c in cryptos:
price = float(c.current_price)
if settings and settings.is_pinning_active and c.manual_price:
price = float(c.manual_price)
symbol_display = c.symbol if 'USDT' in c.symbol else f"{c.symbol}USDT"
data.append({ data.append({
'symbol': c.symbol, 'symbol': symbol_display,
'price': float(c.current_price), 'price': price,
'change': float(c.change_24h) 'change': float(c.change_24h)
}) })
return JsonResponse(data, safe=False) return JsonResponse(data, safe=False)