262 lines
9.8 KiB
HTML
262 lines
9.8 KiB
HTML
{% extends "portal/base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}充值 - cmhub{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex flex-column gap-4">
|
|
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
|
<div>
|
|
<h1 class="h3 mb-1">充值</h1>
|
|
<div class="text-secondary">{{ user.email }}</div>
|
|
</div>
|
|
<a class="btn btn-outline-secondary" href="{% url 'portal-recharge-records' %}">充值记录</a>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<div class="metric">
|
|
<div class="text-secondary small">当前点数</div>
|
|
<div class="display-6 fw-semibold">{{ balance.points_balance }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="metric">
|
|
<div class="text-secondary small">充值总额</div>
|
|
<div class="display-6 fw-semibold">{{ recharge_total_amount|floatformat:2 }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="metric">
|
|
<div class="text-secondary small">入账点数</div>
|
|
<div class="display-6 fw-semibold">{{ recharge_points_total }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="cmhub-surface">
|
|
<h2 class="h5 mb-3">发起充值</h2>
|
|
<form method="post" class="row g-3 align-items-end" novalidate>
|
|
{% csrf_token %}
|
|
{% if form.non_field_errors %}
|
|
<div class="col-12">
|
|
<div class="alert alert-danger mb-0">{{ form.non_field_errors|striptags }}</div>
|
|
</div>
|
|
{% endif %}
|
|
<div class="col-md-5">
|
|
<label class="form-label" for="{{ form.amount.id_for_label }}">{{ form.amount.label }}</label>
|
|
{{ form.amount }}
|
|
{% if form.amount.errors %}
|
|
<div class="text-danger small mt-1">{{ form.amount.errors|striptags }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="form-label">{{ form.pay_method.label }}</div>
|
|
<div class="d-flex gap-3 flex-wrap">
|
|
{% for radio in form.pay_method %}
|
|
<div class="form-check">
|
|
{{ radio.tag }}
|
|
<label class="form-check-label" for="{{ radio.id_for_label }}">{{ radio.choice_label }}</label>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% if form.pay_method.errors %}
|
|
<div class="text-danger small mt-1">{{ form.pay_method.errors|striptags }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="col-md-3">
|
|
<button class="btn btn-primary w-100" type="submit">创建订单</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
|
|
{% if current_order %}
|
|
<section
|
|
class="cmhub-surface"
|
|
data-recharge-order
|
|
data-order-no="{{ current_order.order_no }}"
|
|
data-status="{{ current_order.status }}"
|
|
data-code-url="{{ current_order.code_url }}"
|
|
data-status-url="{% url 'api-recharge-status' %}"
|
|
>
|
|
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-3">
|
|
<div>
|
|
<h2 class="h5 mb-1">待支付订单</h2>
|
|
<div class="text-secondary small">订单号:<code>{{ current_order.order_no }}</code></div>
|
|
</div>
|
|
<span class="badge text-bg-secondary" id="recharge-status-badge">{{ current_order.status }}</span>
|
|
</div>
|
|
|
|
{% if is_mock_payment_mode %}
|
|
<div class="alert alert-warning" role="alert">
|
|
当前为支付测试模式。下方二维码只用于联调订单创建、页面渲染和状态轮询,不能使用微信或支付宝真实付款。
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="row g-4 align-items-start">
|
|
<div class="col-md-5">
|
|
<canvas id="recharge-qr" class="border rounded bg-white p-2" width="240" height="240"></canvas>
|
|
<div id="recharge-qr-fallback" class="d-none mt-3">
|
|
<label class="form-label" for="recharge-code-url">支付票据</label>
|
|
<textarea id="recharge-code-url" class="form-control font-monospace key-value" rows="4" readonly>{{ current_order.code_url }}</textarea>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-7">
|
|
<dl class="row mb-0">
|
|
<dt class="col-sm-4">金额</dt>
|
|
<dd class="col-sm-8">{{ current_order.amount_money|floatformat:2 }} {{ current_order.currency }}</dd>
|
|
<dt class="col-sm-4">预计到账</dt>
|
|
<dd class="col-sm-8">{{ current_order.points_granted }} 点</dd>
|
|
<dt class="col-sm-4">支付方式</dt>
|
|
<dd class="col-sm-8">{{ current_order.pay_method }}</dd>
|
|
<dt class="col-sm-4">创建时间</dt>
|
|
<dd class="col-sm-8">{{ current_order.created_at|date:"Y-m-d H:i" }}</dd>
|
|
<dt class="col-sm-4">二维码有效期</dt>
|
|
<dd class="col-sm-8">{{ current_order.expires_at|date:"Y-m-d H:i"|default:"-" }}</dd>
|
|
<dt class="col-sm-4">到账时间</dt>
|
|
<dd class="col-sm-8" id="recharge-paid-at">{{ current_order.paid_at|date:"Y-m-d H:i"|default:"-" }}</dd>
|
|
</dl>
|
|
<div class="alert alert-info mt-3 mb-0" id="recharge-poll-hint" aria-live="polite">
|
|
{% if current_order.status == "pending" %}
|
|
正在等待支付结果,页面会自动轮询订单状态。
|
|
{% elif current_order.status == "paid" %}
|
|
订单已到账,余额已刷新。
|
|
{% else %}
|
|
订单当前状态为 {{ current_order.status }}。
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<section class="cmhub-surface">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h2 class="h5 mb-0">最近充值</h2>
|
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-recharge-records' %}">全部</a>
|
|
</div>
|
|
{% if recent_recharge_orders %}
|
|
<div class="table-responsive">
|
|
<table class="table align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">订单号</th>
|
|
<th scope="col">金额</th>
|
|
<th scope="col">点数</th>
|
|
<th scope="col">状态</th>
|
|
<th scope="col">创建时间</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for order in recent_recharge_orders %}
|
|
<tr>
|
|
<td><code>{{ order.order_no }}</code></td>
|
|
<td>{{ order.amount_money|floatformat:2 }} {{ order.currency }}</td>
|
|
<td>{{ order.points_granted }}</td>
|
|
<td>{{ order.status }}</td>
|
|
<td>{{ order.created_at|date:"Y-m-d H:i" }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-secondary">暂无充值记录</div>
|
|
{% endif %}
|
|
</section>
|
|
</div>
|
|
|
|
<script src="{% static 'portal/vendor/qrcode/qrcode.js' %}"></script>
|
|
<script>
|
|
(function () {
|
|
const panel = document.querySelector("[data-recharge-order]");
|
|
if (!panel) {
|
|
return;
|
|
}
|
|
|
|
const orderNo = panel.dataset.orderNo;
|
|
const codeUrl = panel.dataset.codeUrl;
|
|
const statusUrl = panel.dataset.statusUrl;
|
|
const statusBadge = document.getElementById("recharge-status-badge");
|
|
const paidAt = document.getElementById("recharge-paid-at");
|
|
const pollHint = document.getElementById("recharge-poll-hint");
|
|
const fallback = document.getElementById("recharge-qr-fallback");
|
|
const canvas = document.getElementById("recharge-qr");
|
|
const labels = {
|
|
pending: "pending",
|
|
paid: "paid",
|
|
failed: "failed",
|
|
expired: "expired"
|
|
};
|
|
|
|
function showQrFallback() {
|
|
if (fallback) {
|
|
fallback.classList.remove("d-none");
|
|
}
|
|
}
|
|
|
|
if (codeUrl && window.QRCode && typeof window.QRCode.toCanvas === "function") {
|
|
window.QRCode.toCanvas(canvas, codeUrl, { width: 240, margin: 1 }, function (error) {
|
|
if (error) {
|
|
showQrFallback();
|
|
}
|
|
});
|
|
} else {
|
|
showQrFallback();
|
|
}
|
|
|
|
function setStatus(data) {
|
|
const status = data.status || "pending";
|
|
statusBadge.textContent = labels[status] || status;
|
|
statusBadge.className = status === "paid" ? "badge text-bg-success" : "badge text-bg-secondary";
|
|
paidAt.textContent = data.paid_at ? new Date(data.paid_at).toLocaleString() : "-";
|
|
if (status === "paid") {
|
|
pollHint.textContent = "订单已到账,正在刷新余额。";
|
|
} else if (status === "pending" && data.is_expired) {
|
|
pollHint.textContent = "二维码已过期,可重新发起一笔充值;若已支付,到账仍以回调为准。";
|
|
} else if (status === "pending") {
|
|
pollHint.textContent = "正在等待支付结果,页面会自动轮询订单状态。";
|
|
} else {
|
|
pollHint.textContent = "订单当前状态为 " + status + "。";
|
|
}
|
|
}
|
|
|
|
function pollStatus() {
|
|
fetch(statusUrl + "?order_no=" + encodeURIComponent(orderNo), {
|
|
method: "GET",
|
|
credentials: "same-origin",
|
|
headers: { Accept: "application/json" }
|
|
})
|
|
.then(function (response) {
|
|
return response.json().then(function (data) {
|
|
if (!response.ok) {
|
|
throw new Error((data.error && data.error.message) || "status request failed");
|
|
}
|
|
return data;
|
|
});
|
|
})
|
|
.then(function (data) {
|
|
setStatus(data);
|
|
if (data.status === "paid") {
|
|
window.setTimeout(function () {
|
|
window.location.reload();
|
|
}, 1200);
|
|
return;
|
|
}
|
|
if (data.status === "pending") {
|
|
window.setTimeout(pollStatus, 1000);
|
|
}
|
|
})
|
|
.catch(function () {
|
|
pollHint.textContent = "状态查询失败,稍后自动重试。";
|
|
window.setTimeout(pollStatus, 3000);
|
|
});
|
|
}
|
|
|
|
if (panel.dataset.status === "pending") {
|
|
window.setTimeout(pollStatus, 1000);
|
|
}
|
|
}());
|
|
</script>
|
|
{% endblock %}
|