321 lines
13 KiB
JavaScript
321 lines
13 KiB
JavaScript
/* 当前商品选择页:选套餐 → 选支付方式 → 下单 → 支付 → 自动激活本机 */
|
||
const $ = (id) => document.getElementById(id);
|
||
let packages = [];
|
||
let selected = null;
|
||
let orderNo = null;
|
||
let pollTimer = null;
|
||
let lastPayType = null; // 记住当前支付方式,「重新下单」时直接复用
|
||
let payHelpShown = false;
|
||
let helpTipStage = 0; // 1=30秒提示 2=120秒提示;同阶段不重复覆盖刚点出来的反馈
|
||
const PENDING_KEY = 'biliBuyPendingOrder';
|
||
|
||
/** 订单号落本地:关掉页面再回来也能接着查卡,不会「付了钱拿不到码」 */
|
||
function savePending() {
|
||
try {
|
||
localStorage.setItem(PENDING_KEY, JSON.stringify({
|
||
orderNo, payType: lastPayType,
|
||
cardTypeId: selected ? selected.id : null, at: Date.now()
|
||
}));
|
||
} catch (e) { /* 隐私模式下不可用则忽略 */ }
|
||
}
|
||
|
||
function clearPending() {
|
||
try { localStorage.removeItem(PENDING_KEY); } catch (e) {}
|
||
}
|
||
|
||
function readPending(maxAgeMs) {
|
||
try {
|
||
const d = JSON.parse(localStorage.getItem(PENDING_KEY) || 'null');
|
||
if (!d || !d.orderNo) return null;
|
||
if (!d.at || Date.now() - d.at > maxAgeMs) { clearPending(); return null; }
|
||
return d;
|
||
} catch (e) { return null; }
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({
|
||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||
}[c]));
|
||
}
|
||
|
||
function durationText(days) {
|
||
const n = Number(days);
|
||
if (!n) return '限时体验';
|
||
if (n >= 999999) return '永久有效';
|
||
return n + ' 天';
|
||
}
|
||
|
||
async function loadPackages() {
|
||
$('package-list').innerHTML = '<div class="loading">正在加载当前商品…</div>';
|
||
try {
|
||
const res = await chrome.runtime.sendMessage({ action: 'getCardTypes' });
|
||
if (!res || !res.ok) throw new Error((res && res.message) || '加载失败');
|
||
packages = (res.data || []).filter((p) => Number(p.price) > 0);
|
||
if (!packages.length) {
|
||
$('package-list').innerHTML = '<div class="empty">当前暂无在售商品,请稍后再试</div>';
|
||
return;
|
||
}
|
||
renderPackages();
|
||
} catch (e) {
|
||
$('package-list').innerHTML = '<div class="empty">商品加载失败:' + esc(e.message) + '</div>';
|
||
}
|
||
}
|
||
|
||
function renderPackages() {
|
||
$('package-list').innerHTML = packages.map((p) => `
|
||
<div class="package" data-id="${esc(p.id)}">
|
||
<div class="pkg-name">${esc(p.name)}</div>
|
||
<div class="pkg-price">¥${esc(p.price)}</div>
|
||
<div class="pkg-dur">${esc(durationText(p.duration_days))}</div>
|
||
<button class="pkg-btn" data-id="${esc(p.id)}">选择此商品</button>
|
||
</div>`).join('');
|
||
document.querySelectorAll('.pkg-btn').forEach((btn) => {
|
||
btn.addEventListener('click', () => selectPackage(btn.dataset.id));
|
||
});
|
||
}
|
||
|
||
function selectPackage(id) {
|
||
selected = packages.find((p) => String(p.id) === String(id));
|
||
if (!selected) return;
|
||
document.querySelectorAll('.package').forEach((el) => {
|
||
el.classList.toggle('active', el.dataset.id === String(id));
|
||
});
|
||
$('chosen-package').innerHTML = `已选商品:<b>${esc(selected.name)}</b> 价格 <b>¥${esc(selected.price)}</b> ${esc(durationText(selected.duration_days))}`;
|
||
$('step-pay').hidden = false;
|
||
$('step-pay').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
|
||
async function createOrder(payType) {
|
||
if (!selected) return;
|
||
lastPayType = payType;
|
||
$('order-panel').hidden = false;
|
||
$('order-panel').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
hidePayHelp();
|
||
const status = $('order-status');
|
||
status.className = 'order-status pending';
|
||
status.textContent = '⏳ 正在创建订单…';
|
||
try {
|
||
const res = await chrome.runtime.sendMessage({
|
||
action: 'authCreateOrder',
|
||
payType,
|
||
cardTypeId: selected.id
|
||
});
|
||
if (!res || !res.ok) throw new Error((res && res.message) || '创建订单失败');
|
||
|
||
orderNo = res.order.orderNo;
|
||
savePending();
|
||
status.textContent = `订单 ${orderNo}(¥${res.order.amount})已创建,正在打开支付页面…`;
|
||
if (res.payUrl) window.open(res.payUrl, '_blank');
|
||
startPolling();
|
||
} catch (e) {
|
||
status.className = 'order-status error';
|
||
status.textContent = '✗ ' + e.message;
|
||
}
|
||
}
|
||
|
||
let attempts = 0;
|
||
|
||
/** 单次订单查询:5 秒轮询与「立即刷新核对」按钮共用同一段逻辑 */
|
||
async function queryOrderOnce() {
|
||
attempts++;
|
||
if (attempts > 240) {
|
||
clearInterval(pollTimer);
|
||
pollTimer = null;
|
||
const status = $('order-status');
|
||
status.className = 'order-status error';
|
||
status.textContent = `✗ 等待支付超时。订单号 ${orderNo},如已付款请把订单号发给官网客服处理。`;
|
||
return;
|
||
}
|
||
try {
|
||
const res = await chrome.runtime.sendMessage({ action: 'authQueryOrder', orderNo });
|
||
if (!res || !res.ok) return;
|
||
// 只要拿到卡密就先展示出来,不等激活结果——自动激活失败时用户手里必须有码
|
||
if (res.cardKey) showCardBox(res.cardKey, !!res.activated);
|
||
|
||
if (res.activated) {
|
||
clearInterval(pollTimer);
|
||
pollTimer = null;
|
||
clearPending();
|
||
showActivated(res);
|
||
} else if (res.cardKey) {
|
||
clearInterval(pollTimer);
|
||
pollTimer = null;
|
||
hidePayHelp();
|
||
clearPending();
|
||
const status = $('order-status');
|
||
status.className = 'order-status error';
|
||
status.innerHTML = '✗ <b>已支付并已发卡</b>,但自动激活未成功(' +
|
||
esc(res.message || '本机绑定可能被占用') + ')。请用上方卡密手动激活:回到插件侧边栏 → 卡密输入框粘贴 → 点「粘贴卡密并激活」。<br>' +
|
||
'订单号:<b>' + esc(orderNo || '') + '</b>';
|
||
} else if (res.paid) {
|
||
// 已付款但服务端还没把卡发出来(最常见原因是卡池库存不足)
|
||
const waited = attempts * 5;
|
||
const status = $('order-status');
|
||
status.className = 'order-status pending';
|
||
status.textContent = '⏳ 已支付,正在为你发卡…(订单 ' + orderNo + ',已等待 ' + waited + ' 秒)';
|
||
if (waited >= 90) {
|
||
status.className = 'order-status error';
|
||
status.innerHTML = '⏳ <b>已支付</b>,但发卡耗时较长(已 ' + waited + ' 秒)。订单 <b>' + esc(orderNo) +
|
||
'</b> 付款状态为已支付,<b>权益不会丢失</b>:继续等会自动发放,或凭此订单号联系客服补发。';
|
||
}
|
||
} else {
|
||
const waited = attempts * 5;
|
||
const status = $('order-status');
|
||
status.className = 'order-status pending';
|
||
status.textContent = '⏳ 等待支付中…(订单 ' + orderNo + ',已等待 ' + waited + ' 秒)';
|
||
// 30 秒还没动静就主动给出口(付款通知丢失 / 扫了过期码 / 实际没扣款)
|
||
if (waited >= 30) showPayHelp(waited);
|
||
}
|
||
} catch (e) {
|
||
/* 单次查询失败不中断轮询 */
|
||
}
|
||
}
|
||
|
||
function startPolling() {
|
||
if (pollTimer) clearInterval(pollTimer);
|
||
attempts = 0;
|
||
hidePayHelp();
|
||
queryOrderOnce(); // 先立刻查一次,不干等 5 秒
|
||
pollTimer = setInterval(queryOrderOnce, 5000);
|
||
}
|
||
|
||
/** 付款迟迟没动静时给出路,别让用户对着「等待支付中」干等 */
|
||
function showPayHelp(waited) {
|
||
const box = $('pay-help');
|
||
if (!box) return;
|
||
box.hidden = false;
|
||
payHelpShown = true;
|
||
const tip = $('ph-tip');
|
||
if (!tip) return;
|
||
const stage = waited >= 120 ? 2 : 1;
|
||
if (tip.textContent && stage <= helpTipStage) return; // 不覆盖用户刚点「刷新」得到的反馈
|
||
helpTipStage = stage;
|
||
if (stage === 2) {
|
||
tip.textContent = '已等待 ' + waited + ' 秒仍未确认支付。若你确已完成付款:请勿重复支付,留好订单号 ' +
|
||
(orderNo || '') + ' 与微信/支付宝交易号;若二维码是旧的/已过期,点「重新下单」换一个新码再付。';
|
||
} else {
|
||
tip.textContent = '正在等待付款通知…已完成付款的话,点「立即刷新核对」可以马上重查一次。';
|
||
}
|
||
}
|
||
|
||
function hidePayHelp() {
|
||
const box = $('pay-help');
|
||
if (box) box.hidden = true;
|
||
payHelpShown = false;
|
||
helpTipStage = 0;
|
||
const tip = $('ph-tip');
|
||
if (tip) tip.textContent = '';
|
||
}
|
||
|
||
/** 打开购买页时恢复上次未完成的订单:关掉页面也不会「付了钱拿不到码」 */
|
||
function resumePending() {
|
||
const d = readPending(30 * 60 * 1000);
|
||
if (!d) return;
|
||
orderNo = d.orderNo;
|
||
if (d.payType) lastPayType = d.payType;
|
||
if (d.cardTypeId && !selected) {
|
||
const p = packages.find((x) => String(x.id) === String(d.cardTypeId));
|
||
if (p) {
|
||
selected = p;
|
||
document.querySelectorAll('.package').forEach((el) => {
|
||
el.classList.toggle('active', el.dataset.id === String(d.cardTypeId));
|
||
});
|
||
$('chosen-package').innerHTML = `已选商品:<b>${esc(p.name)}</b> 价格 <b>¥${esc(p.price)}</b> ${esc(durationText(p.duration_days))}`;
|
||
$('step-pay').hidden = false;
|
||
}
|
||
}
|
||
$('order-panel').hidden = false;
|
||
const st = $('order-status');
|
||
st.className = 'order-status pending';
|
||
st.textContent = '⏳ 已恢复上次未完成的订单 ' + orderNo + ',正在查询支付结果…';
|
||
startPolling();
|
||
}
|
||
|
||
function wirePayHelp() {
|
||
const rf = $('ph-refresh'), rn = $('ph-renew');
|
||
if (rf) rf.addEventListener('click', async () => {
|
||
const tip = $('ph-tip');
|
||
if (tip) tip.textContent = '正在向授权系统核对…';
|
||
await queryOrderOnce();
|
||
const box = $('pay-help');
|
||
if (!box || box.hidden || !tip) return; // 已出卡激活就不必再提示
|
||
tip.textContent = '已于 ' + new Date().toLocaleTimeString() +
|
||
' 重新核对:仍未确认支付。若你确已完成付款请勿重复支付(通知可能延迟);二维码过期就点「重新下单」。';
|
||
});
|
||
if (rn) rn.addEventListener('click', () => {
|
||
if (!selected) {
|
||
const tip = $('ph-tip');
|
||
if (tip) tip.textContent = '请先在上方重新选择套餐,再选支付方式生成新订单。';
|
||
const sp = $('step-package');
|
||
if (sp) sp.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
return;
|
||
}
|
||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||
createOrder(lastPayType || 'wxpay');
|
||
});
|
||
}
|
||
|
||
/** 支付成功后立刻把卡密展示给用户(含自动激活失败的场景) */
|
||
function showCardBox(key, activated) {
|
||
const box = $('card-box');
|
||
if (!box || !key) return;
|
||
if (!box.hidden && $('cb-key-text').textContent === key) return;
|
||
$('cb-key-text').textContent = key;
|
||
box.hidden = false;
|
||
$('cb-copy-tip').textContent = activated ? '已自动激活本机,这是你的卡密,建议留存' : '';
|
||
box.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
|
||
function showActivated(res) {
|
||
hidePayHelp();
|
||
const status = $('order-status');
|
||
const permanent = !!res.isPermanent;
|
||
const exp = String(res.expireTime || '').trim();
|
||
const plan = String(res.planName || '').trim();
|
||
// 绝不默认「永久」:到期时间只在服务端明确给出时才显示
|
||
let expireLine;
|
||
if (permanent) expireLine = '到期时间:<b>永久</b>';
|
||
else if (exp) expireLine = '到期时间:<b>' + esc(exp) + '</b>';
|
||
else if (plan) expireLine = '套餐:<b>' + esc(plan) + '</b>(实际到期时间以插件侧边栏「已授权 · 到期 …」为准)';
|
||
else expireLine = '到期时间:以插件侧边栏显示为准';
|
||
status.className = 'order-status success';
|
||
status.innerHTML = `
|
||
<div class="ok-title">✓ 支付成功,已自动激活</div>
|
||
<div class="ok-line">卡密:<b>${esc(res.cardKey || '')}</b></div>
|
||
<div class="ok-line">${expireLine}</div>
|
||
<div class="ok-line muted">回到插件侧边栏即可使用全部功能(本机已授权)。</div>`;
|
||
}
|
||
|
||
document.querySelectorAll('.pay-btn').forEach((btn) => {
|
||
btn.addEventListener('click', () => createOrder(btn.dataset.pay));
|
||
});
|
||
|
||
// 复制卡密:优先 clipboard API,无权限时降级 execCommand
|
||
$('cb-copy') && $('cb-copy').addEventListener('click', async () => {
|
||
const key = ($('cb-key-text').textContent || '').trim();
|
||
if (!key) return;
|
||
let ok = false;
|
||
try {
|
||
await navigator.clipboard.writeText(key);
|
||
ok = true;
|
||
} catch (e) { /* 降级 */ }
|
||
if (!ok) {
|
||
try {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = key;
|
||
ta.style.position = 'fixed';
|
||
ta.style.opacity = '0';
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
ok = document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
} catch (e2) {
|
||
ok = false;
|
||
}
|
||
}
|
||
$('cb-copy-tip').textContent = ok ? '已复制,可直接粘贴到侧边栏激活' : '复制失败,请手动长按选择卡密';
|
||
});
|
||
|
||
wirePayHelp();
|
||
loadPackages().then(resumePending);
|