This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no,email=no" />
|
||||
<title>订单订阅恢复</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f5f7fb;
|
||||
--panel: #ffffff;
|
||||
--text: #162033;
|
||||
--muted: #667085;
|
||||
--line: #d8dee9;
|
||||
--primary: #2563eb;
|
||||
--primary-pressed: #1d4ed8;
|
||||
--danger: #d92d20;
|
||||
--success: #047857;
|
||||
--shadow: 0 18px 48px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at 50% -10%, rgba(37, 99, 235, 0.16), transparent 34rem),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: max(20px, env(safe-area-inset-top)) 16px
|
||||
max(24px, env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(100%, 440px);
|
||||
}
|
||||
|
||||
.brand {
|
||||
margin-bottom: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.brand p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid rgba(216, 222, 233, 0.86);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 0 13px;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.code-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 116px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 46px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
padding: 0 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.12s ease,
|
||||
background 0.12s ease,
|
||||
opacity 0.12s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:not(:disabled):hover {
|
||||
background: var(--primary-pressed);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e9eefb;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 14px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: none;
|
||||
margin: 14px 0 0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.status.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
border: 1px solid rgba(4, 120, 87, 0.24);
|
||||
background: rgba(4, 120, 87, 0.08);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status.error {
|
||||
border: 1px solid rgba(217, 45, 32, 0.24);
|
||||
background: rgba(217, 45, 32, 0.08);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.page {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.code-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<section class="shell" aria-labelledby="page-title">
|
||||
<div class="brand">
|
||||
<h1 id="page-title">订单订阅恢复</h1>
|
||||
<p>填写订单号并验证邮箱,系统会为该邮箱恢复对应订阅。</p>
|
||||
</div>
|
||||
|
||||
<form class="card" id="recovery-form">
|
||||
<div class="field">
|
||||
<label for="order-no">订单号</label>
|
||||
<input
|
||||
id="order-no"
|
||||
name="order_no"
|
||||
autocomplete="off"
|
||||
inputmode="text"
|
||||
placeholder="请输入订单号"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">邮箱</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
inputmode="email"
|
||||
placeholder="请输入接收验证码的邮箱"
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="code">邮箱验证码</label>
|
||||
<div class="code-row">
|
||||
<input
|
||||
id="code"
|
||||
name="code"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
maxlength="8"
|
||||
placeholder="请输入验证码"
|
||||
required
|
||||
/>
|
||||
<button class="btn-secondary" id="send-code" type="button">
|
||||
获取验证码
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary" id="submit-btn" type="submit">
|
||||
恢复订阅
|
||||
</button>
|
||||
|
||||
<div class="status" id="status" role="status"></div>
|
||||
|
||||
<p class="hint">
|
||||
恢复成功后,请使用该邮箱验证码登录账号查看订阅。每个订单号只能恢复一次。
|
||||
</p>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById("recovery-form");
|
||||
const sendCodeButton = document.getElementById("send-code");
|
||||
const submitButton = document.getElementById("submit-btn");
|
||||
const statusBox = document.getElementById("status");
|
||||
const emailInput = document.getElementById("email");
|
||||
const orderInput = document.getElementById("order-no");
|
||||
const codeInput = document.getElementById("code");
|
||||
|
||||
let countdownTimer = null;
|
||||
|
||||
function showStatus(type, message) {
|
||||
statusBox.className = `status show ${type}`;
|
||||
statusBox.textContent = message;
|
||||
}
|
||||
|
||||
function clearStatus() {
|
||||
statusBox.className = "status";
|
||||
statusBox.textContent = "";
|
||||
}
|
||||
|
||||
function normalizeEmail() {
|
||||
emailInput.value = emailInput.value.trim().toLowerCase();
|
||||
return emailInput.value;
|
||||
}
|
||||
|
||||
async function postJson(path, body) {
|
||||
const response = await fetch(path, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload.code !== 200) {
|
||||
throw new Error(payload.msg || "请求失败,请稍后再试");
|
||||
}
|
||||
return payload.data || {};
|
||||
}
|
||||
|
||||
function startCountdown(seconds) {
|
||||
let remaining = seconds;
|
||||
sendCodeButton.disabled = true;
|
||||
sendCodeButton.textContent = `${remaining}s`;
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = setInterval(() => {
|
||||
remaining -= 1;
|
||||
if (remaining <= 0) {
|
||||
clearInterval(countdownTimer);
|
||||
sendCodeButton.disabled = false;
|
||||
sendCodeButton.textContent = "获取验证码";
|
||||
return;
|
||||
}
|
||||
sendCodeButton.textContent = `${remaining}s`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
sendCodeButton.addEventListener("click", async () => {
|
||||
clearStatus();
|
||||
const email = normalizeEmail();
|
||||
if (!emailInput.reportValidity()) return;
|
||||
|
||||
sendCodeButton.disabled = true;
|
||||
sendCodeButton.textContent = "发送中";
|
||||
try {
|
||||
await postJson("/v1/public/recovery/send_code", { email });
|
||||
showStatus("success", "验证码已发送,请查看邮箱。");
|
||||
startCountdown(60);
|
||||
} catch (error) {
|
||||
sendCodeButton.disabled = false;
|
||||
sendCodeButton.textContent = "获取验证码";
|
||||
showStatus("error", error.message || "验证码发送失败,请稍后再试。");
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearStatus();
|
||||
const orderNo = orderInput.value.trim();
|
||||
const email = normalizeEmail();
|
||||
const code = codeInput.value.trim();
|
||||
if (!form.reportValidity()) return;
|
||||
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = "恢复中";
|
||||
try {
|
||||
const result = await postJson("/v1/public/recovery/order", {
|
||||
order_no: orderNo,
|
||||
email,
|
||||
code
|
||||
});
|
||||
showStatus(
|
||||
"success",
|
||||
result.message || "恢复完成,请使用该邮箱验证码登录查看订阅。"
|
||||
);
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
showStatus("error", "订单或邮箱验证码无效,请确认后重试。");
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = "恢复订阅";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user