Files
cmhub/apps/portal/templates/portal/recharge.html
T

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 %}