400 lines
11 KiB
HTML
400 lines
11 KiB
HTML
<!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");
|
|
const API_BASE = "https://api.hifast.biz";
|
|
|
|
let countdownTimer = null;
|
|
|
|
const ERROR_MESSAGE_MAP = {
|
|
"Param Error": "请检查订单号、邮箱和验证码是否填写正确。",
|
|
"Verify code error": "邮箱验证码错误或已过期,请重新获取验证码。",
|
|
"Too Many Requests": "验证码发送太频繁,请稍后再试。",
|
|
"This account has reached the limit of sending times today":
|
|
"该邮箱今天获取验证码次数已达上限,请明天再试。"
|
|
};
|
|
|
|
function friendlyMessage(message, fallback) {
|
|
return ERROR_MESSAGE_MAP[message] || message || fallback;
|
|
}
|
|
|
|
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(`${API_BASE}${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(
|
|
friendlyMessage(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
|
|
});
|
|
const message =
|
|
result.message || "恢复完成,请使用该邮箱验证码登录查看订阅。";
|
|
if (result.success === false) {
|
|
showStatus("error", message);
|
|
return;
|
|
}
|
|
showStatus("success", message);
|
|
codeInput.value = "";
|
|
} catch (error) {
|
|
showStatus(
|
|
"error",
|
|
error.message || "恢复失败,请检查信息后重试。"
|
|
);
|
|
} finally {
|
|
submitButton.disabled = false;
|
|
submitButton.textContent = "恢复订阅";
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|