feat: add portal recharge page
This commit is contained in:
@@ -83,6 +83,7 @@
|
||||
<div class="ms-auto d-flex gap-2 flex-wrap justify-content-end">
|
||||
{% if user.is_authenticated %}
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-dashboard' %}">控制台</a>
|
||||
<a class="btn btn-sm btn-primary" href="{% url 'portal-recharge' %}">充值</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-apikeys' %}">API Key</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-recharge-records' %}">充值记录</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-usage-records' %}">消费记录</a>
|
||||
|
||||
@@ -54,9 +54,9 @@
|
||||
<div class="metric d-flex flex-column gap-3">
|
||||
<div>
|
||||
<div class="text-secondary small">充值</div>
|
||||
<div class="h5 mb-0">充值记录</div>
|
||||
<div class="h5 mb-0">扫码充值</div>
|
||||
</div>
|
||||
<a class="btn btn-primary align-self-start" href="{% url 'portal-recharge-records' %}">查看</a>
|
||||
<a class="btn btn-primary align-self-start" href="{% url 'portal-recharge' %}">充值</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
{% extends "portal/base.html" %}
|
||||
|
||||
{% 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>
|
||||
|
||||
<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="https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.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 %}
|
||||
Reference in New Issue
Block a user