/* ============================================================
* 云村小助手 - 站点账号系统前端逻辑(按次计费版)
* 未经授权不得复制、修改或用于商业用途
* ============================================================ */
(function () {
'use strict';
/* Promise.finally 老内核 polyfill(与 index.html 同一份标准实现——
index.html 的脚本先加载,这里再兜底一次确保独立引用时也安全) */
if (!Promise.prototype.finally) {
Promise.prototype.finally = function (cb) {
var P = this.constructor || Promise;
return this.then(
function (v) { return P.resolve(cb()).then(function () { return v; }); },
function (e) { return P.resolve(cb()).then(function () { throw e; }); }
);
};
}
var SITE_VERSION = '2.4.0'; // 与后端 APP_VERSION、index.html CHANGELOG 对齐
var CHANGELOG = [
{ v: '2.4.0', date: '2026-09-21', items: ['修复:老内核浏览器(Chrome 57-62)操作按钮卡加载的问题', '安全:登录失败次数过多临时锁定;公网错误信息不再暴露内部细节', '优化:用户列表不再携带明文网易云 Cookie(按需单独查看)'] },
{ v: '2.1.0', date: '2026-08-28', items: ['计费改为按次计费:一首歌 1 次,歌单打包按歌曲数量扣减', '免费试用改为 3 次(3 首歌),注册页领取,每设备限一次', '卡密系统升级为次数卡,后台可管理用户次数'] },
{ v: '2.0.0', date: '2026-08-28', items: ['新增站点账号系统:强制登录、邮箱验证码注册', '新增管理后台:用户管理、全局Cookie、SMTP、日志、卡密、公告、数据库备份', '新增个人中心:剩余次数、卡密充值、公告查看'] },
{ v: '1.2.0', date: '2026-03-29', items: ['修复部分歌曲解析失败的问题', '优化界面交互体验'] }
];
function $(s, root) { return (root || document).querySelector(s); }
function $$(s, root) { return Array.prototype.slice.call((root || document).querySelectorAll(s)); }
/* ---------------- toast ---------------- */
var toastWrap = null;
function toast(msg, type) {
if (!toastWrap) {
toastWrap = document.createElement('div');
toastWrap.style.cssText = 'position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column;gap:8px;align-items:center;pointer-events:none;';
document.body.appendChild(toastWrap);
}
var t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'padding:10px 18px;border-radius:10px;font-size:13px;color:#fff;background:' +
(type === 'error' ? 'linear-gradient(135deg,#ff5f57,#e0483f)' :
type === 'success' ? 'linear-gradient(135deg,#34c759,#2aa64c)' : 'rgba(30,30,30,.92)') +
';box-shadow:0 6px 24px rgba(0,0,0,.25);opacity:0;transform:translateY(-8px);transition:all .25s;max-width:80vw;';
toastWrap.appendChild(t);
requestAnimationFrame(function () { t.style.opacity = '1'; t.style.transform = 'translateY(0)'; });
setTimeout(function () {
t.style.opacity = '0'; t.style.transform = 'translateY(-8px)';
setTimeout(function () { t.remove(); }, 300);
}, 2600);
}
/* ---------------- 请求 ---------------- */
function api(url, params) {
var body = Object.keys(params || {}).map(function (k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(params[k] == null ? '' : params[k]);
}).join('&');
// 30 秒超时:服务端挂起时不再无限等待(登录含省份查询约 5s 上限,留足余量;
// 无 AbortController 的老内核优雅降级为无超时)
var ctl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
var timer = null;
if (ctl) timer = setTimeout(function () { try { ctl.abort(); } catch (e) {} }, 30000);
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
body: body,
credentials: 'same-origin',
signal: ctl ? ctl.signal : undefined
}).then(function (r) {
// 双回调解包(不用 Promise.finally:兼容 Chromium 57+ 老内核)
return r.json().then(function (d) { if (timer) clearTimeout(timer); return d; },
function (e) { if (timer) clearTimeout(timer); throw e; });
}).catch(function () {
// 默认失败兜底——网络断/超时/非 JSON 响应不再产生 unhandledrejection
// 噪音(喂 fe_error 上报通道),统一转为标准失败对象走调用方既有的 toast
if (timer) clearTimeout(timer);
return { status: 0, success: false, message: '网络异常,请检查连接后重试' };
});
}
function apiGet(url) {
var ctl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
var timer = null;
if (ctl) timer = setTimeout(function () { try { ctl.abort(); } catch (e) {} }, 15000);
// 注意:apiGet 不做默认失败兜底——/auth/me 的调用方依赖"失败保持原登录态"
//(拒绝会跳过 then 体,不清空已有 user),转成 resolved 失败对象会让 UI 掉成游客
return fetch(url, { credentials: 'same-origin', signal: ctl ? ctl.signal : undefined })
.then(function (r) {
return r.json().then(function (d) { if (timer) clearTimeout(timer); return d; },
function (e) { if (timer) clearTimeout(timer); throw e; });
});
}
/* ---------------- 设备标识 ---------------- */
function getDeviceId() {
/* 与 index.html 的 getDeviceId 统一为同一键/同一值——
注册指纹与下载任务/心跳的设备号不再各走一套;
内存缓存兜底 localStorage 不可写时同一页面内保持稳定。 */
if (window.__siteDeviceIdCache) return window.__siteDeviceIdCache;
var KEY = 'netease_device_id';
var d = '';
try { d = localStorage.getItem(KEY) || ''; } catch (e) { }
if (!d) {
d = 'dev-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
try { localStorage.setItem(KEY, d); } catch (e) { }
}
window.__siteDeviceIdCache = d;
return d;
}
/* ---------------- 状态 ---------------- */
var user = null;
var durationText = '';
var ann = null;
var smtpOk = false; // fail-closed——/auth/me 返回前不显示注册入口(防止未配置 SMTP 的窗口期误开)
var registerCredits = 3; // 注册赠送次数由 /auth/me 下发,文案不再硬编码
/* ---------------- 弹窗 ---------------- */
function openModal(id) { var m = $('#' + id); if (m) m.classList.add('show'); }
function closeModal(id) { var m = $('#' + id); if (m) m.classList.remove('show'); }
/* ---------------- 导航栏 ---------------- */
function renderAuthArea() {
// 同步全局登录状态,供主脚本 requireLogin() 使用
window.SITE_LOGGED_IN = !!user;
// 未开放邮箱注册(SMTP 未配置)时,隐藏所有注册入口,避免用户注册卡死
var showRegister = !user && smtpOk;
var regBtn = $('#btn-open-register');
if (regBtn) regBtn.hidden = !showRegister;
['goto-register', 'goto-register2'].forEach(function (id) {
var a = $('#' + id); if (a) a.hidden = !showRegister;
});
// 两个弹窗(登录/注册)里的试用提示都要受管——原 querySelector 只命中第一个
$$('.auth-trial-box').forEach(function (box) { box.hidden = !showRegister; });
$$('.reg-gift-num').forEach(function (el) { el.textContent = registerCredits; });
var guest = $('#auth-guest-area'), uarea = $('#auth-user-area');
if (guest && uarea) {
if (user) {
guest.hidden = true; uarea.hidden = false;
var pill = $('#auth-duration-pill');
if (pill) pill.textContent = '⏳ ' + (durationText || '0 次');
var av = $('#user-avatar');
if (av) {
av.textContent = (user.username || 'U').charAt(0).toUpperCase();
av.title = user.username + (user.role === 'admin' ? '(管理员)' : '');
}
} else {
guest.hidden = false; uarea.hidden = true;
}
}
var ucTab = $('#tab-usercenter'), uaTab = $('#tab-useradmin');
if (ucTab) ucTab.hidden = !user;
if (uaTab) uaTab.hidden = !(user && user.role === 'admin');
// 全局账号歌单仅管理员可见
var gTab = $('#tab-global');
if (gTab) gTab.hidden = !(user && user.role === 'admin');
}
/* ---------------- 会话 ---------------- */
function refreshSession() {
return apiGet('/auth/me').then(function (data) {
var d = data.data || {};
user = d.user || null;
durationText = d.duration_text || '';
ann = d.announcement || null;
smtpOk = !!d.smtp_configured;
// 注册赠送次数动态下发(/auth/me),文案不再硬编码
if (typeof d.register_credits === 'number') registerCredits = d.register_credits;
renderAuthArea();
renderUserCenter();
maybeShowAnnouncement();
// 确认登录后再恢复下载任务(游客调任务接口会 401 误弹登录框)
if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') {
try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {}
}
return user;
}).catch(function () { return null; });
}
// 全局暴露给主脚本 apiPost 的 401 统一处理(同一提示 5 秒内不重复弹出)
var _lastAuthToast = 0;
function authToastOnce(msg, type) {
var now = Date.now();
if (now - _lastAuthToast < 5000) return;
_lastAuthToast = now;
toast(msg, type || 'error');
}
window.handleAuthExpired = function (msg, expired) {
if (expired) {
authToastOnce(msg || '下载次数已用尽,请充值卡密');
showExpiredModal();
return;
}
// 会话失效时清掉内存里的旧用户状态再引导登录
//(原实现保留旧 user,页面继续显示"已登录"却处处 401)
user = null;
durationText = '';
renderAuthArea();
renderUserCenter();
authToastOnce(msg || '登录已失效,请重新登录');
// 登录弹窗已打开且正处于找回/两步验证视图时,不复位视图——
// 未登录期间后台任务轮询/心跳会持续吃 401 反复进入本函数,若每次都把
// 弹窗拽回登录视图,管理员正在输入的验证码会被打断、票据被清空
var _lm = document.getElementById('login-modal');
var _fv = document.getElementById('forgot-view');
var _a2v = document.getElementById('admin2fa-view');
var _authViewOpen = _lm && _lm.classList.contains('show') &&
((_fv && !_fv.hidden) || (_a2v && !_a2v.hidden));
if (!_authViewOpen) {
hideForgotView();
hideAdmin2faView();
}
// 会话失效弹登录窗前,先关掉可能叠开的注册弹窗(双遮罩错乱),
// 已 show 则短路——401 风暴(1.5s 轮询)期间不反复重开
var _reg = document.getElementById('register-modal');
if (_reg && _reg.classList.contains('show')) closeModal('register-modal');
if (_lm && _lm.classList.contains('show')) return;
openModal('login-modal');
};
// 暴露给主脚本:扣费/任务结束后刷新顶栏剩余次数与登录态
window.refreshSITECredits = refreshSession;
window.renderSITEUserCenter = renderUserCenter;
// 暴露当前剩余下载次数给主脚本预检(未登录 -1,不限 999999)
window.getSITECredits = function () {
if (!user) return -1;
var c = (typeof user.duration_seconds === 'number') ? user.duration_seconds : 0;
return c < 0 ? 999999 : c;
};
/* 次数用尽弹窗(下载触发,带「去充值」入口) */
function showExpiredModal() {
// 先移除可能残留的旧实例(此前被 Escape 隐藏但未删 DOM 时,旧实现会直接
// return,导致该弹窗从此再也弹不出来)
var old = $('#site-expired-modal');
if (old) old.remove();
var mask = document.createElement('div');
mask.className = 'modal-mask show';
mask.id = 'site-expired-modal';
mask.setAttribute('data-dynamic', '1'); // Escape 关闭时连同 DOM 一并移除
mask.innerHTML =
'
' +
' ' +
'
' +
' 您的下载次数已用完。解析与在线试听免费,下载需要消耗次数(一首歌 = 1 次,歌单打包按歌曲数量扣减)。
' +
' 请在个人中心充值卡密后继续下载。' +
'
' +
'
' +
' ' +
' ' +
'
' +
'
';
document.body.appendChild(mask);
mask.addEventListener('click', function (e) { if (e.target === mask) mask.remove(); });
var go = $('#site-expired-go'), close = $('#site-expired-close');
if (go) go.addEventListener('click', function () {
mask.remove();
var t = $('#tab-usercenter');
if (t) t.click();
});
if (close) close.addEventListener('click', function () { mask.remove(); });
}
function afterAuth(data) {
var d = data.data || {};
user = d.user || null;
durationText = d.duration_text || '';
renderAuthArea();
renderUserCenter();
// 登录成功后立即恢复本设备的进行中任务
if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') {
try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {}
}
}
/* ---------------- 登录 / 注册弹窗 ---------------- */
// 注册唯一入口——导航按钮与登录弹窗两处链接(全站仅有的 3 条打开
// 路径)统一经此:未同意协议 → cookieGate('register') 弹「隐私与协议设置」
// 注册模式(16 秒强制阅读计时)并中止;已同意 → 直接进注册弹窗。
// 提交侧 doRegister 另有 fail-closed 兜底闸,程序化打开也绕不过。
function openRegisterFlow() {
closeModal('login-modal'); // 先关登录弹窗,避免协议窗被压在下方
var gate = window.SITE_COOKIE_GATE;
if (typeof gate !== 'function') {
// fail-closed:协议闸未就绪(主脚本异常)时不放行注册
toast('页面协议组件未就绪,请刷新页面后重试', 'error');
return;
}
if (!gate('register')) return; // 未同意:闸内已弹协议窗并提示
openModal('register-modal');
}
function bindAuthModal() {
var lo = $('#btn-open-login'), ro = $('#btn-open-register');
if (lo) lo.addEventListener('click', function () { closeModal('register-modal'); hideForgotView(); hideAdmin2faView(); openModal('login-modal'); });
if (ro) ro.addEventListener('click', function () { closeModal('login-modal'); openRegisterFlow(); });
['login-close', 'register-close', 'announce-close'].forEach(function (id) {
var b = $('#' + id);
if (b) b.addEventListener('click', function () {
if (id.indexOf('login') === 0 && window.resetAuthViews) window.resetAuthViews(); // 关闭侧复位
closeModal(id.indexOf('login') === 0 ? 'login-modal' : id.indexOf('register') === 0 ? 'register-modal' : 'announce-modal');
});
});
[['login-modal', 'login-close'], ['register-modal', 'register-close']].forEach(function (p) {
var m = $('#' + p[0]);
if (m) m.addEventListener('click', function (e) {
if (e.target === m) {
if (p[0] === 'login-modal' && window.resetAuthViews) window.resetAuthViews(); // 关闭侧复位
closeModal(p[0]);
}
});
});
var g1 = $('#goto-register'), g2 = $('#goto-register2');
if (g1) g1.addEventListener('click', function () { closeModal('login-modal'); openRegisterFlow(); });
if (g2) g2.addEventListener('click', function () { closeModal('login-modal'); openRegisterFlow(); });
var gl1 = $('#goto-login');
if (gl1) gl1.addEventListener('click', function () { closeModal('register-modal'); hideForgotView(); hideAdmin2faView(); openModal('login-modal'); });
// 找回密码入口/返回(只交付 DOM 漏了绑定)
var gf = $('#goto-forgot');
if (gf) gf.addEventListener('click', showForgotView);
var fbl = $('#forgot-back-login');
if (fbl) fbl.addEventListener('click', hideForgotView);
// 管理员两步验证视图的返回链接
var a2b = $('#a2fa-back');
if (a2b) a2b.addEventListener('click', hideAdmin2faView);
// 注册即送 3 次,无需 Tab 切换
}
/* ---------------- 登录 ---------------- */
function doLogin() {
var btn = $('#login-submit');
var username = ($('#login-username') || {}).value || '';
var password = ($('#login-password') || {}).value || '';
if (!username || !password) { toast('请输入用户名/邮箱和密码', 'error'); return; }
btn.disabled = true; btn.textContent = '登录中...';
api('/auth/login', { username: username, password: password })
.then(function (data) {
if (data.status !== 200) throw new Error(data.message || '登录失败');
// 管理员密码验证通过——转两步验证视图(服务端未建会话)
if (data.data && data.data.admin_2fa) {
showAdmin2faView(data.data.ticket, data.data.email_masked);
toast('密码验证通过,请完成邮箱验证', 'success');
return;
}
afterAuth(data);
closeModal('login-modal');
toast('登录成功,欢迎回来,' + (user ? user.username : ''), 'success');
if (data.data && data.data.redirect === 'admin') {
var uaTab = $('#tab-useradmin');
if (uaTab) uaTab.click();
} else {
var ucTab = $('#tab-usercenter');
if (ucTab) ucTab.click();
}
})
.catch(function (e) { toast(e.message || '登录失败', 'error'); })
.finally(function () { btn.disabled = false; btn.textContent = '登 录'; });
}
/* ---------------- 注册 ---------------- */
var _REG_EMAIL_RE = /^[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}$/;
var _REG_NAME_RE = /^[A-Za-z0-9_\u4e00-\u9fa5]{3,20}$/;
// 注册弹窗内常驻错误行(长文案 2.6s toast 看不完)
function regShowError(msg) {
toast(msg, 'error');
var line = $('#reg-error-line');
if (line) { line.textContent = msg; line.hidden = false; }
}
function regClearError() {
var line = $('#reg-error-line');
if (line) { line.textContent = ''; line.hidden = true; }
}
function sendCode() {
var btn = $('#reg-send-code');
var email = ($('#reg-email') || {}).value || '';
if (!_REG_EMAIL_RE.test(email.trim())) { toast('邮箱格式不正确', 'error'); return; }
regClearError();
btn.disabled = true; btn.textContent = '发送中...';
api('/auth/send_code', { email: email, purpose: 'register' })
.then(function (data) {
if (data.status !== 200) throw new Error(data.message || '发送失败');
toast('验证码已发送,请查收邮箱', 'success');
var left = 60;
window.__regCodeTimer = setInterval(function () {
left--; btn.textContent = left + 's';
if (left <= 0) { clearInterval(window.__regCodeTimer); btn.textContent = '发送验证码'; btn.disabled = false; }
}, 1000);
})
.catch(function (e) {
toast(e.message || '发送失败', 'error');
btn.disabled = false; btn.textContent = '发送验证码';
});
}
/* ---------------- 找回密码 ---------------- */
// 只交付了找回视图 DOM 未实现 JS——此处补齐全链路。
// 后端 /auth/forgot_password 已就绪:验证码原子消费、防枚举
// 统一文案、login_seq 踢全会话、5 次/分/IP 限流;发码复用 /auth/send_code。
function forgotShowError(msg) {
toast(msg, 'error');
var line = $('#forgot-error-line');
if (line) { line.textContent = msg; line.hidden = false; }
}
function forgotClearError() {
var line = $('#forgot-error-line');
if (line) { line.textContent = ''; line.hidden = true; }
}
function forgotStopCodeTimer() {
clearInterval(window.__forgotCodeTimer || 0);
var btn = $('#forgot-send-code');
if (btn && btn.disabled) { btn.disabled = false; btn.textContent = '发送验证码'; }
}
function showForgotView() {
var form = document.querySelector('#login-modal .auth-form');
var fv = $('#forgot-view');
if (!form || !fv) return;
form.classList.add('forgot-mode');
fv.hidden = false;
forgotClearError();
var em = $('#forgot-email');
if (em) { try { em.focus(); } catch (e) {} }
}
function hideForgotView() {
var form = document.querySelector('#login-modal .auth-form');
var fv = $('#forgot-view');
if (form) form.classList.remove('forgot-mode');
if (fv && !fv.hidden) fv.hidden = true;
forgotClearError();
forgotStopCodeTimer();
}
function doForgotSendCode() {
var btn = $('#forgot-send-code');
var email = ($('#forgot-email') || {}).value || '';
if (!_REG_EMAIL_RE.test(email.trim())) { forgotShowError('邮箱格式不正确'); return; }
forgotClearError();
btn.disabled = true; btn.textContent = '发送中...';
api('/auth/send_code', { email: email, purpose: 'reset' })
.then(function (data) {
if (data.status !== 200) throw new Error(data.message || '发送失败');
toast('验证码已发送,请查收邮箱', 'success');
var left = 60;
window.__forgotCodeTimer = setInterval(function () {
left--; btn.textContent = left + 's';
if (left <= 0) { clearInterval(window.__forgotCodeTimer); btn.textContent = '发送验证码'; btn.disabled = false; }
}, 1000);
})
.catch(function (e) {
forgotShowError(e.message || '发送失败');
btn.disabled = false; btn.textContent = '发送验证码';
});
}
function doForgotReset() {
var btn = $('#forgot-submit');
var email = ($('#forgot-email') || {}).value || '';
var code = ($('#forgot-code') || {}).value || '';
var password = ($('#forgot-password') || {}).value || '';
forgotClearError();
if (!email || !code || !password) { forgotShowError('请填写完整找回信息'); return; }
if (!_REG_EMAIL_RE.test(email.trim())) { forgotShowError('邮箱格式不正确'); return; }
if (password.length < 8 || password.length > 128) { forgotShowError('新密码长度需在 8-128 位之间'); return; }
btn.disabled = true; btn.textContent = '重置中...';
api('/auth/forgot_password', { email: email, code: code, password: password })
.then(function (data) {
if (data.status !== 200) throw new Error(data.message || '重置失败');
var fe = $('#forgot-email'), fc = $('#forgot-code'), fp = $('#forgot-password');
if (fe) fe.value = ''; if (fc) fc.value = ''; if (fp) fp.value = '';
forgotStopCodeTimer();
hideForgotView();
toast('密码已重置,请用新密码登录', 'success');
})
.catch(function (e) { forgotShowError(e.message || '重置失败'); })
.finally(function () { btn.disabled = false; btn.textContent = '重置密码'; });
}
/* ---------------- 管理员两步验证 ----------------
* Step1 用户名+密码 → 服务端签发票据(不建会话);本视图完成
* Step2 绑定邮箱比对(触发发码)与 Step3 验证码登录。普通用户不经过此流程。 */
var _a2faTicket = '';
function a2faShowError(msg) {
toast(msg, 'error');
var line = $('#a2fa-error-line');
if (line) { line.textContent = msg; line.hidden = false; }
}
function a2faClearError() {
var line = $('#a2fa-error-line');
if (line) { line.textContent = ''; line.hidden = true; }
}
function a2faStopTimer() {
clearInterval(window.__a2faTimer || 0);
var btn = $('#a2fa-send');
if (btn && btn.disabled) { btn.disabled = false; btn.textContent = '发送验证码'; }
}
function showAdmin2faView(ticket, emailMask) {
var form = document.querySelector('#login-modal .auth-form');
var v = $('#admin2fa-view');
if (!form || !v) return;
_a2faTicket = ticket || '';
var mask = $('#a2fa-email-mask');
if (mask) mask.textContent = emailMask || '-';
// 清掉上次会话残留的输入(错码/旧邮箱不再被原样重复提交)
var ae = $('#a2fa-email'), ac = $('#a2fa-code');
if (ae) ae.value = '';
if (ac) ac.value = '';
form.classList.add('admin2fa-mode');
v.hidden = false;
a2faClearError();
a2faStopTimer();
var em = $('#a2fa-email');
if (em) { try { em.focus(); } catch (e) {} }
}
function hideAdmin2faView() {
var form = document.querySelector('#login-modal .auth-form');
var v = $('#admin2fa-view');
if (form) form.classList.remove('admin2fa-mode');
if (v && !v.hidden) v.hidden = true;
_a2faTicket = '';
a2faClearError();
a2faStopTimer();
}
// 关闭侧统一复位——登录弹窗的 X/遮罩/Escape/doLogout 关闭路径都调用,
// 不再只依赖"打开侧纪律";同时供 index.html 的 Escape 通用分支使用
window.resetAuthViews = function () { hideForgotView(); hideAdmin2faView(); };
function doA2faSendCode() {
var btn = $('#a2fa-send');
var email = ($('#a2fa-email') || {}).value || '';
if (!email.trim()) { a2faShowError('请输入管理员绑定邮箱'); return; }
a2faClearError();
btn.disabled = true; btn.textContent = '发送中...';
api('/auth/admin_login/email', { ticket: _a2faTicket, email: email })
.then(function (data) {
if (data.status === 401) {
// 先复位视图再提示——错误行随视图隐藏,此处 toast 是唯一可达提示
hideAdmin2faView();
a2faShowError(data.message || '请重新登录');
return;
}
if (data.status !== 200) throw new Error(data.message || '发送失败');
toast('验证码已发送至管理员邮箱,请查收', 'success');
var ci = $('#a2fa-code');
if (ci) { try { ci.focus(); } catch (e) {} }
var left = 60;
window.__a2faTimer = setInterval(function () {
left--; btn.textContent = left + 's';
if (left <= 0) { clearInterval(window.__a2faTimer); btn.textContent = '发送验证码'; btn.disabled = false; }
}, 1000);
})
.catch(function (e) {
a2faShowError(e.message || '发送失败');
btn.disabled = false; btn.textContent = '发送验证码';
});
}
function doA2faVerify() {
var btn = $('#a2fa-submit');
var email = ($('#a2fa-email') || {}).value || '';
var code = ($('#a2fa-code') || {}).value || '';
a2faClearError();
if (!email.trim()) { a2faShowError('请输入管理员绑定邮箱'); return; }
if (!code.trim()) { a2faShowError('请先获取并输入邮箱验证码'); return; }
btn.disabled = true; btn.textContent = '验证中...';
api('/auth/admin_login/verify', { ticket: _a2faTicket, code: code })
.then(function (data) {
if (data.status === 401) {
// 先复位视图再提示(同上,错误行随视图隐藏)
hideAdmin2faView();
a2faShowError(data.message || '请重新登录');
return;
}
if (data.status !== 200) throw new Error(data.message || '验证失败');
a2faClearError();
afterAuth(data);
closeModal('login-modal');
toast('登录成功,欢迎回来,' + (user ? user.username : ''), 'success');
var uaTab = $('#tab-useradmin');
if (uaTab) uaTab.click();
})
.catch(function (e) { a2faShowError(e.message || '验证失败'); })
.finally(function () { btn.disabled = false; btn.textContent = '验证并登录'; });
}
function doRegister() {
var btn = $('#reg-submit');
var username = ($('#reg-username') || {}).value || '';
var password = ($('#reg-password') || {}).value || '';
var email = ($('#reg-email') || {}).value || '';
var code = ($('#reg-code') || {}).value || '';
regClearError();
// 提交侧兜底闸(入口已统一走 openRegisterFlow,此处防任何程序化
// 打开路径绕过)。fail-closed:协议闸缺失视为未同意。注册语境只查协议同意
// (功能性 Cookie 不拦注册,拦解析/下载时另有提示);未同意时闸内已自动
// 弹「隐私与协议设置」注册模式并 toast,此处不再重复提示
if (typeof window.SITE_COOKIE_GATE !== 'function') {
toast('页面协议组件未就绪,请刷新页面后重试', 'error');
return;
}
if (!window.SITE_COOKIE_GATE('register')) return;
// 前端即时校验(口径与后端一致,省一次网络往返)
if (!username || !password || !email || !code) { regShowError('请填写完整注册信息'); return; }
if (!_REG_NAME_RE.test(username)) { regShowError('用户名需 3-20 位,仅支持中文、字母、数字、下划线,且不能纯下划线'); return; }
if (!_REG_EMAIL_RE.test(email.trim())) { regShowError('邮箱格式不正确'); return; }
if (password.length < 8 || password.length > 128) { regShowError('密码长度需在 8-128 位之间'); return; }
btn.disabled = true; btn.textContent = '注册中...';
api('/auth/register', {
username: username, password: password, email: email, code: code,
device_id: getDeviceId(), agree: 1
})
.then(function (data) {
if (data.status !== 200) throw new Error(data.message || '注册失败');
regClearError();
afterAuth(data);
closeModal('register-modal');
toast('注册成功,已自动登录,赠送的下载次数可在个人中心查看', 'success');
var ucTab = $('#tab-usercenter');
if (ucTab) ucTab.click();
})
.catch(function (e) { regShowError(e.message || '注册失败'); })
.finally(function () { btn.disabled = false; btn.textContent = '注 册'; });
}
/* ---------------- 退出登录 ---------------- */
function doLogout() {
api('/auth/logout', {}).then(function () {
user = null; durationText = '';
renderAuthArea();
renderUserCenter(); // 同步个人中心 gate/content 显隐,与其他登出路径一致
if (window.resetAuthViews) window.resetAuthViews(); // 登出复位登录弹窗视图
closeModal('register-modal'); closeModal('login-modal');
// 退出连带清除个人网易云登录(index.html 注册的钩子:
// 清 localStorage 的个人 Cookie/解析账号选择并复位账号栏)
if (typeof window.SITE_ON_LOGOUT === 'function') {
try { window.SITE_ON_LOGOUT(); } catch (e) {}
}
var ex = $('#site-expired-modal'); if (ex) ex.remove();
// 切回搜索页,避免停留在个人中心 / 管理后台 / 管理员歌单
var search = document.querySelector('.tab[data-tab="search"]') || $('#tab-search');
if (search) search.click();
toast('已退出登录', 'success');
});
}
/* ---------------- 个人中心 ---------------- */
function fmtTime(ts) {
if (!ts) return '-';
var d = new Date(ts * 1000);
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
}
function renderUserCenter() {
var el;
// 登录态切换:隐藏 🔒 占位、显示个人中心内容
var gate = $('#uc-gate'), content = $('#uc-content');
if (gate && content) {
gate.hidden = !!user;
content.hidden = !user;
}
if (!user) return;
if ((el = $('#uc-avatar'))) el.textContent = (user.username || 'U').charAt(0).toUpperCase();
if ((el = $('#uc-username'))) el.textContent = user ? user.username : '-';
if ((el = $('#uc-role'))) el.textContent = user ? (user.role === 'admin' ? '管理员 · 不限次数'
: (typeof user.duration_seconds === 'number' && user.duration_seconds < 0 ? 'VIP · 不限次数' : '普通用户')) : '-';
if ((el = $('#uc-duration'))) el.textContent = user ? (durationText || '0 次') : '-';
if ((el = $('#uc-email'))) el.textContent = (user && user.email) || '未绑定';
if ((el = $('#uc-created'))) el.textContent = user ? fmtTime(user.created_at) : '-';
var cl = $('#uc-changelog');
if (cl) {
cl.innerHTML = CHANGELOG.map(function (v) {
return 'v' + v.v + ' ' +
v.date + '' +
v.items.map(function (i) { return '· ' + i; }).join('
') + '
';
}).join('') + '当前版本 v' + SITE_VERSION +
'
';
}
}
function doRedeem() {
var input = $('#uc-card-input');
var code = (input && input.value || '').trim();
if (!code) { toast('请输入卡密', 'error'); return; }
var btn = $('#uc-card-redeem');
btn.disabled = true;
api('/auth/redeem', { code: code })
.then(function (data) {
if (data.status !== 200) throw new Error(data.message || '兑换失败');
durationText = (data.data && data.data.duration_text) || durationText;
// 同步更新内存余额(批量下载预检读的是 user.duration_seconds,
// 原实现只改显示文字,预检仍用充值前的旧值)
if (user && data.data && typeof data.data.duration_seconds === 'number') {
user.duration_seconds = data.data.duration_seconds;
return null;
}
return refreshSession();
})
.then(function () {
renderAuthArea(); renderUserCenter();
if (input) input.value = '';
toast('充值成功', 'success');
})
.catch(function (e) { toast(e.message || '兑换失败', 'error'); })
.finally(function () { btn.disabled = false; });
}
/* ---------------- 公告 ---------------- */
function maybeShowAnnouncement(force) {
// 手动"查看公告"(force=true)无条件展示;
// 自动弹出仍按 popup_mode 与 once 已读版本判断
if (!force && (!ann || !ann.content || ann.popup_mode === 'off')) return;
if (!force && ann && ann.popup_mode === 'once') {
// once 模式的已读判断:登录用户比对服务端 ann_seen;
// 游客比对本地 localStorage(原实现只查 user,游客每次刷新都重复弹窗)
if (user && user.ann_seen && String(user.ann_seen) === String(ann.updated_at)) return;
if (!user) {
var guestSeen = '';
try { guestSeen = localStorage.getItem('netease_ann_once_seen') || ''; } catch (e) {}
if (guestSeen === String(ann.updated_at)) return;
}
}
if (!ann || !ann.content) { toast('当前没有公告内容', 'info'); return; }
var t = $('#announce-title'), c = $('#announce-content');
if (t) t.textContent = ann.title || '系统公告';
if (c) c.innerHTML = String(ann.content).split('\n').map(function (l) { return escapeHtml(l); }).join('
');
// 首访 Cookie 协议窗显示中不叠加公告弹窗(双遮罩错乱);
// 跳过时不记已读,下次进入页面再弹
var _cm = document.getElementById('cookie-modal');
if (_cm && _cm.classList.contains('show')) return;
openModal('announce-modal');
if (user) api('/auth/ann_seen', {});
else { try { localStorage.setItem('netease_ann_once_seen', String(ann.updated_at)); } catch (e) {} }
}
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
/* ---------------- 管理后台 ---------------- */
var uaLoaded = false;
var uaLogsType = 'success';
function uaShowGate() {
var gate = $('#ua-gate'), content = $('#ua-content');
if (!gate || !content) return;
var isAdmin = !!(user && user.role === 'admin');
gate.hidden = isAdmin;
content.hidden = !isAdmin;
if (isAdmin && !uaLoaded) { uaLoaded = true; uaLoadUsers(); uaLoadCookieStatus(); uaLoadSmtp(); uaLoadLogs(); uaLoadCards(); uaLoadAnn(); uaLoadDb(); }
}
function uaBindSide() {
$$('#admin-side .admin-side-item[data-view]').forEach(function (item) {
// 侧栏项补键盘语义(同型——tabindex/role + Enter/空格激活)
item.setAttribute('tabindex', '0');
item.setAttribute('role', 'button');
item.addEventListener('click', function () {
$$('#admin-side .admin-side-item').forEach(function (i) { i.classList.remove('active'); });
item.classList.add('active');
$$('.admin-view').forEach(function (v) { v.classList.toggle('active', v.getAttribute('data-aview') === item.getAttribute('data-view')); });
});
item.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); item.click(); }
});
});
var back = $('#ua-back');
if (back) back.addEventListener('click', function () {
var t = $('#tab-useradmin'); if (t) t.classList.remove('active');
var search = document.querySelector('.tab[data-tab="search"]');
if (search) search.click();
});
var lo = $('#ua-logout');
if (lo) lo.addEventListener('click', doLogout);
// 返回/退出两个无 data-view 的侧栏项同样补键盘语义
[back, lo].forEach(function (el) {
if (!el) return;
el.setAttribute('tabindex', '0');
el.setAttribute('role', 'button');
el.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); }
});
});
/* ---- 播放 API 密钥 ---- */
var pkShow = $('#ua-playkey-show');
var pkInput = $('#ua-playkey-value');
function pkCopyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () { toast('密钥已复制', 'success'); },
function () { toast('复制失败,请手动选择文本复制', 'error'); });
} else {
toast('复制失败,请点击「显示密钥」后手动复制', 'error');
}
}
if (pkShow) pkShow.addEventListener('click', function () {
if (pkShow.dataset.state === 'shown') {
pkInput.value = '';
pkInput.placeholder = '点击「显示密钥」查看';
pkShow.textContent = '显示密钥';
pkShow.dataset.state = '';
return;
}
api('/admin/site/play_key/get', {}).then(function (d) {
if (d.status !== 200) { toast(d.message || '获取失败', 'error'); return; }
pkInput.value = d.data.key;
pkShow.textContent = '隐藏';
pkShow.dataset.state = 'shown';
});
});
var pkCopy = $('#ua-playkey-copy');
if (pkCopy) pkCopy.addEventListener('click', function () {
api('/admin/site/play_key/get', {}).then(function (d) {
if (d.status !== 200) { toast(d.message || '获取失败', 'error'); return; }
pkCopyToClipboard(d.data.key);
});
});
var pkRotate = $('#ua-playkey-rotate');
if (pkRotate) pkRotate.addEventListener('click', function () {
if (!confirm('刷新后旧密钥立即全部失效(外部旧地址将打不开),确定继续?')) return;
api('/admin/site/play_key/rotate', {}).then(function (d) {
if (d.status !== 200) { toast(d.message || '刷新失败', 'error'); return; }
pkInput.value = d.data.key;
pkShow.textContent = '隐藏';
pkShow.dataset.state = 'shown';
toast('密钥已刷新,旧密钥已全部失效', 'success');
});
});
}
/* ---- 用户管理 ---- */
function uaInjectUserToolbar() {
var refresh = $('#ua-users-refresh');
if (!refresh || $('#ua-user-create')) return;
var btn = document.createElement('button');
btn.className = 'btn btn-sm btn-primary';
btn.id = 'ua-user-create';
btn.type = 'button';
btn.textContent = '➕ 添加用户';
btn.addEventListener('click', uaCreateUserModal);
refresh.parentNode.insertBefore(btn, refresh);
}
function uaCreateUserModal() {
var old = $('#site-create-user-modal');
if (old) old.remove();
var mask = document.createElement('div');
mask.className = 'modal-mask show';
mask.id = 'site-create-user-modal';
mask.setAttribute('data-dynamic', '1'); // Escape 关闭时连同 DOM 一并移除
mask.innerHTML =
'';
document.body.appendChild(mask);
mask.addEventListener('click', function (e) { if (e.target === mask) mask.remove(); });
var cancel = $('#cnu-cancel');
if (cancel) cancel.addEventListener('click', function () { mask.remove(); });
var submit = $('#cnu-submit');
if (submit) submit.addEventListener('click', function () {
var body = {
username: ($('#cnu-username') || {}).value || '',
password: ($('#cnu-password') || {}).value || '',
email: ($('#cnu-email') || {}).value || '',
count: ($('#cnu-count') || {}).value || 0
};
if (!body.username || body.password.length < 8) { toast('请填写用户名和至少 8 位密码', 'error'); return; }
submit.disabled = true;
api('/admin/site/user/create', body).then(function (d) {
toast(d.message || (d.status === 200 ? '创建成功' : '创建失败'), d.status === 200 ? 'success' : 'error');
if (d.status === 200) { mask.remove(); uaLoadUsers(); }
}).finally(function () { submit.disabled = false; });
});
}
function uaLoadUsers() {
uaInjectUserToolbar();
api('/admin/site/users', {}).then(function (data) {
var body = $('#ua-users-body');
if (!body) return;
if (data.status !== 200) { body.innerHTML = '| ' + escapeHtml(data.message || '加载失败') + ' |
'; return; }
var users = (data.data && data.data.users) || [];
var cnt = $('#ua-users-count');
if (cnt) cnt.textContent = users.length;
if (!users.length) { body.innerHTML = '| 暂无用户 |
'; return; }
body.innerHTML = users.map(function (u) {
var isAdmin = u.role === 'admin';
var disabled = (u.status === 0);
var banned = (u.status === 2);
// 数值字段同过转义(纵深防御——后端口径若变为字符串
// 不再形成管理后台存储型 XSS 面,与同表 username/email 口径对齐)
var uid = escapeHtml(String(u.id));
var dur = escapeHtml(String(u.duration_seconds));
var cred = u.duration_seconds < 0 ? '不限' : (dur + ' 次');
var dev = [u.register_ip, u.trial_device].filter(Boolean).join(' · ') || '-'; // IP 优先显示
if (u.last_province) dev += ' · ' + u.last_province;
return '' +
'| ' + uid + ' | ' +
'' + escapeHtml(u.username) +
(banned ? ' 已封禁' : '') +
(disabled ? ' 已禁用' : '') + ' | ' +
'' + escapeHtml(u.email || '-') + ' | ' +
'' + (isAdmin ? '管理员' : '用户') + ' | ' +
'' + cred + ' | ' +
'' + escapeHtml(dev) + ' | ' +
' | ' +
'' +
' ' +
(isAdmin ? '' :
' ' +
' ' +
' ' +
'') +
' |
';
}).join('');
$$('[data-uedit]', body).forEach(function (b) {
b.addEventListener('click', function () {
var v = prompt('设置该用户的剩余次数(-1 表示不限):', b.getAttribute('data-cur'));
if (v === null) return;
api('/admin/site/user/duration', { id: b.getAttribute('data-uedit'), count: v })
.then(function (d) { toast(d.message || (d.status === 200 ? '已修改' : '修改失败'), d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); });
});
});
// 管理员自助改密(需验证旧密码;改完其他设备全部下线)
$$('[data-uself]', body).forEach(function (b) {
b.addEventListener('click', function () {
var oldpwd = prompt('请输入当前密码:', '');
if (oldpwd === null) return;
var newpwd = prompt('请输入新密码(至少 8 位):', '');
if (newpwd === null) return;
api('/admin/site/user/reset_password', {
id: b.getAttribute('data-uself'), password: newpwd, old_password: oldpwd
}).then(function (d) {
toast(d.message || (d.status === 200 ? '密码已修改' : '修改失败'), d.status === 200 ? 'success' : 'error');
});
});
});
$$('[data-upwd]', body).forEach(function (b) {
b.addEventListener('click', function () {
var v = prompt('为用户「' + b.getAttribute('data-name') + '」设置新密码(至少 8 位):', '');
if (v === null) return;
api('/admin/site/user/reset_password', { id: b.getAttribute('data-upwd'), password: v })
.then(function (d) { toast(d.message || (d.status === 200 ? '已重置' : '重置失败'), d.status === 200 ? 'success' : 'error'); });
});
});
$$('[data-utoggle]', body).forEach(function (b) {
b.addEventListener('click', function () {
api('/admin/site/user/toggle', { id: b.getAttribute('data-utoggle') })
.then(function (d) { toast(d.message || '操作成功', d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); });
});
});
$$('[data-uban]', body).forEach(function (b) {
b.addEventListener('click', function () {
var name = b.getAttribute('data-name');
if (b.textContent === '封禁') {
var reason = prompt('封禁用户「' + name + '」\n请输入封禁原因(将展示给用户):', '违规使用,封禁处理');
if (reason === null) return;
api('/admin/site/user/ban', { id: b.getAttribute('data-uban'), action: 'ban', reason: reason })
.then(function (d) { toast(d.message || (d.status === 200 ? '已封禁' : '操作失败'), d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); });
} else {
if (!window.confirm('解封用户「' + name + '」?解封后可正常登录使用。')) return;
api('/admin/site/user/ban', { id: b.getAttribute('data-uban'), action: 'unban' })
.then(function (d) { toast(d.message || (d.status === 200 ? '已解封' : '操作失败'), d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); });
}
});
});
$$('[data-udel]', body).forEach(function (b) {
b.addEventListener('click', function () {
if (!window.confirm('确定删除用户「' + b.getAttribute('data-name') + '」?')) return;
api('/admin/site/user/delete', { id: b.getAttribute('data-udel') })
.then(function (d) { toast(d.message || '已删除', d.status === 200 ? 'success' : 'error'); uaLoadUsers(); });
});
});
$$('[data-ucookie]', body).forEach(function (b) {
b.addEventListener('click', function () {
api('/admin/site/user/cookie', { username: b.getAttribute('data-ucookie') })
.then(function (d) {
if (d.status !== 200) { toast(d.message || '获取失败', 'error'); return; }
var ck = (d.data && d.data.cookie) || '';
if (!ck) { toast('该用户未登录过网易云账号', 'info'); return; }
prompt('用户网易云 Cookie(Ctrl+C 复制):', ck);
});
});
});
});
}
/* ---- 全局 Cookie ---- */
var uaQrTimer = null, uaUnikey = '', uaQrGen = 0; // 轮询代次令牌(删除 Cookie 后作废旧轮询)
function uaLoadCookieStatus() {
api('/admin/cookie/status', {}).then(function (data) {
var el = $('#ua-cookie-status');
if (!el) return;
if (data.status !== 200) { el.textContent = data.message || '获取失败'; return; }
var info = data.data || {};
el.textContent = (info.is_valid ? '✅ 有效' : '❌ 未登录') +
(info.cookie_count != null ? ' · Cookie ' + info.cookie_count + ' 个' : '');
});
}
function uaQrCreate() {
api('/admin/qr/create', {}).then(function (data) {
if (data.status !== 200) { toast(data.message || '生成二维码失败', 'error'); return; }
var d = data.data || {};
uaUnikey = d.unikey || '';
var box = $('#ua-qr-box'), tip = $('#ua-qr-tip');
if (d.qr_svg) {
var uri = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(d.qr_svg)));
box.innerHTML = '
';
}
if (tip) tip.textContent = '请使用网易云音乐手机 APP 扫码';
if (uaQrTimer) clearInterval(uaQrTimer);
uaQrGen++; // 新轮询代次
uaQrTimer = setInterval(uaQrPoll, 3000);
});
}
function uaQrPoll() {
if (!uaUnikey) { if (uaQrTimer) clearInterval(uaQrTimer); return; }
var myGen = uaQrGen; // 本次轮询所属代次
api('/admin/qr/check', { unikey: uaUnikey }).then(function (data) {
if (myGen !== uaQrGen) return; // 期间已删除 Cookie/重开扫码,丢弃迟到响应
var tip = $('#ua-qr-tip');
if (data.status !== 200) {
if (uaQrTimer) clearInterval(uaQrTimer);
if (tip) tip.textContent = data.message || '检查失败';
return;
}
var st = data.data && data.data.status;
if (st === 'waiting') { if (tip) tip.textContent = '等待扫码...'; }
else if (st === 'scanned') { if (tip) tip.textContent = '✅ 已扫码,请在手机上确认'; }
else if (st === 'success') {
if (uaQrTimer) clearInterval(uaQrTimer);
if (tip) tip.textContent = '🎉 登录成功,Cookie 已保存';
toast('全局账号登录成功', 'success');
uaLoadCookieStatus();
} else {
if (uaQrTimer) clearInterval(uaQrTimer);
if (tip) tip.textContent = '二维码已过期,请重新生成';
}
});
}
function uaCookieDelete() {
if (!window.confirm('确定删除全局 Cookie?删除后需重新扫码登录。')) return;
api('/admin/cookie/delete', {}).then(function (data) {
// 作废在途扫码轮询——迟到的 803 成功响应不得把刚删除的 Cookie 写回
if (uaQrTimer) clearInterval(uaQrTimer);
uaUnikey = '';
uaQrGen++;
toast(data.message || '已删除', data.status === 200 ? 'success' : 'error');
var box = $('#ua-qr-box'), tip = $('#ua-qr-tip');
if (box) box.innerHTML = '';
if (tip) tip.textContent = '点击下方按钮生成二维码';
uaLoadCookieStatus();
});
}
/* ---- SMTP ---- */
function uaLoadSmtp() {
api('/admin/site/smtp/get', {}).then(function (data) {
if (data.status !== 200) return;
var d = data.data || {};
if ($('#ua-smtp-host')) $('#ua-smtp-host').value = d.host || '';
if ($('#ua-smtp-port')) $('#ua-smtp-port').value = d.port || 587;
if ($('#ua-smtp-sender')) $('#ua-smtp-sender').value = d.sender || '';
});
}
function uaSaveSmtp() {
api('/admin/site/smtp/save', {
host: ($('#ua-smtp-host') || {}).value || '',
port: ($('#ua-smtp-port') || {}).value || 587,
sender: ($('#ua-smtp-sender') || {}).value || '',
password: ($('#ua-smtp-pass') || {}).value || ''
}).then(function (d) {
toast(d.message || '已保存', d.status === 200 ? 'success' : 'error');
var p = $('#ua-smtp-pass'); if (p) p.value = '';
});
}
function uaTestSmtp() {
var to = ($('#ua-smtp-test-to') || {}).value || '';
if (!to) { toast('请填写测试收件邮箱', 'error'); return; }
var btn = $('#ua-smtp-test');
btn.disabled = true;
api('/admin/site/smtp/test', { to: to }).then(function (d) {
toast(d.message || '已发送', d.status === 200 ? 'success' : 'error');
}).finally(function () { btn.disabled = false; });
}
/* ---- 日志 ---- */
var uaLogPage = 1;
function uaLoadLogs() {
api('/admin/site/logs', { type: uaLogsType, page: uaLogPage }).then(function (data) {
var box = $('#ua-log-list');
if (!box) return;
if (data.status !== 200) { box.textContent = data.message || '加载失败'; return; }
var logs = (data.data && data.data.logs) || [];
var total = (data.data && data.data.total) || 0;
var pages = (data.data && data.data.pages) || 1;
uaLogPage = (data.data && data.data.page) || 1;
window._uaLogPages = pages; // next 按钮按真实页数封顶
var el;
if ((el = $('#ua-log-page'))) el.textContent = uaLogPage;
if ((el = $('#ua-log-pages'))) el.textContent = pages;
if ((el = $('#ua-log-total'))) el.textContent = total;
if (!logs.length) { box.textContent = '暂无日志'; return; }
box.innerHTML = logs.map(function (l) {
return '[' + escapeHtml(l.time) + '] ' + escapeHtml(l.username || '-') +
' · ' + escapeHtml(l.action) + ' · IP ' + escapeHtml(l.ip) +
(l.detail ? ' · ' + escapeHtml(l.detail) : '') + '
';
}).join('');
});
}
function uaClearLogs() {
var name = uaLogsType === 'fail' ? '失败' : '成功';
if (!window.confirm('确定清除全部「' + name + '」日志?该操作不可恢复。')) return;
api('/admin/site/logs/clear', { type: uaLogsType }).then(function (d) {
toast(d.message || (d.status === 200 ? '已清除' : '清除失败'), d.status === 200 ? 'success' : 'error');
uaLogPage = 1;
uaLoadLogs();
});
}
/* ---- 卡密 ---- */
function uaGenCards() {
var creditsVal = ($('#ua-card-minutes') || {}).value || '10';
if (creditsVal === 'custom') {
creditsVal = ($('#ua-card-custom') || {}).value || '0';
if (!(parseInt(creditsVal, 10) > 0)) { toast('请填写自定义次数(正整数)', 'error'); return; }
}
api('/admin/site/cards/generate', {
count: ($('#ua-card-count') || {}).value || 1,
credits: creditsVal,
prefix: ($('#ua-card-prefix') || {}).value || 'VIP',
note: ($('#ua-card-note') || {}).value || '',
valid_days: ($('#ua-card-days') || {}).value || 0
}).then(function (data) {
if (data.status !== 200) { toast(data.message || '生成失败', 'error'); return; }
var box = $('#ua-card-codes');
if (box) {
box.hidden = false;
box.textContent = (data.data.codes || []).join('\n');
}
toast('已生成 ' + (data.data.count || 0) + ' 张卡密(' + (data.data.credits || 0) + ' 次/张),已复制到文本框,请及时保存', 'success');
uaLoadCards(0);
});
}
// 卡密管理操作列(禁用/启用/删除)+ 分页(后端已支持,
// 不分页则超出最新 100 张的旧卡密在界面上永远管理不到)
var _uaCardPage = 0;
var _uaCardTotal = 0;
var UA_CARD_PAGE_SIZE = 100;
function uaCardAction(act, code) {
if (!code) return;
if (act === 'delete' && !window.confirm('确定删除卡密 ' + code.slice(0, 4) + '**** 吗?删除后不可恢复。')) return;
var url = act === 'delete' ? '/admin/site/cards/delete' : '/admin/site/cards/disable';
var body = { code: code, disabled: act === 'enable' ? '0' : '1' };
api(url, body).then(function (data) {
toast(data.message || (data.status === 200 ? '操作成功' : '操作失败'),
data.status === 200 ? 'success' : 'error');
if (data.status === 200) uaLoadCards();
});
}
function _uaRenderCardPager() {
var pager = $('#ua-cards-pager');
if (!pager) return;
if (_uaCardTotal <= 0) { pager.hidden = true; return; }
var pages = Math.max(1, Math.ceil(_uaCardTotal / UA_CARD_PAGE_SIZE));
pager.hidden = false;
pager.textContent = '';
var prev = document.createElement('button');
prev.className = 'btn btn-sm'; prev.type = 'button'; prev.textContent = '上一页';
prev.disabled = _uaCardPage <= 0;
prev.addEventListener('click', function () { uaLoadCards(_uaCardPage - 1); });
var next = document.createElement('button');
next.className = 'btn btn-sm'; next.type = 'button'; next.textContent = '下一页';
next.disabled = _uaCardPage >= pages - 1;
next.addEventListener('click', function () { uaLoadCards(_uaCardPage + 1); });
var info = document.createElement('span');
info.className = 'hint';
info.textContent = '第 ' + (_uaCardPage + 1) + ' / ' + pages + ' 页 · 共 ' + _uaCardTotal + ' 张';
pager.appendChild(prev);
pager.appendChild(info);
pager.appendChild(next);
}
function uaLoadCards(page) {
if (typeof page === 'number') _uaCardPage = Math.max(0, page);
api('/admin/site/cards/list', { limit: UA_CARD_PAGE_SIZE, offset: _uaCardPage * UA_CARD_PAGE_SIZE })
.then(function (data) {
var body = $('#ua-cards-body');
if (!body) return;
if (data.status !== 200) { body.innerHTML = '| 加载失败 |
'; return; }
var cards = (data.data && data.data.cards) || [];
_uaCardTotal = (data.data && data.data.total) || 0;
if (!cards.length) {
body.innerHTML = '| 暂无卡密 |
';
_uaRenderCardPager();
return;
}
body.innerHTML = cards.map(function (c) {
var st, act;
if (c.used_by) {
st = '已使用';
act = '—'; // 兑换凭证保留,不给删除/禁用
} else if (c.disabled) {
st = '已禁用';
act = ' ' +
'';
} else {
// 过期判定用服务端下发的 expired(客户端时钟偏差会误标)
st = c.expired ? '已过期' : '未使用';
act = ' ' +
'';
}
return '| ' + escapeHtml(c.code) + ' | ' + escapeHtml(String(c.minutes)) + ' 次 | ' + st + ' | ' + escapeHtml(c.used_by || '-') + ' | ' +
escapeHtml(c.note || '-') + ' | ' +
(c.expire_at ? new Date(c.expire_at * 1000).toLocaleDateString() : '永久') + ' | ' +
escapeHtml(c.created_at ? new Date(c.created_at * 1000).toLocaleDateString() : '-') + ' | ' + act + ' |
';
}).join('');
_uaRenderCardPager();
Array.prototype.forEach.call(body.querySelectorAll('button[data-card-act]'), function (b) {
b.addEventListener('click', function () {
uaCardAction(b.getAttribute('data-card-act'), b.getAttribute('data-card-code'));
});
});
});
}
/* ---- 公告 ---- */
function uaLoadAnn() {
api('/admin/site/ann/get', {}).then(function (data) {
if (data.status !== 200) return;
var d = data.data || {};
if ($('#ua-ann-title')) $('#ua-ann-title').value = d.title || '';
if ($('#ua-ann-content')) $('#ua-ann-content').value = d.content || '';
if ($('#ua-ann-mode')) $('#ua-ann-mode').value = d.popup_mode || 'off';
});
}
function uaSaveAnn() {
api('/admin/site/ann/save', {
title: ($('#ua-ann-title') || {}).value || '',
content: ($('#ua-ann-content') || {}).value || '',
popup_mode: ($('#ua-ann-mode') || {}).value || 'off'
}).then(function (d) { toast(d.message || '已保存', d.status === 200 ? 'success' : 'error'); });
}
/* ---- 数据库 ---- */
function fmtSize(n) {
if (!n && n !== 0) return '-';
if (n > 1048576) return (n / 1048576).toFixed(2) + ' MB';
if (n > 1024) return (n / 1024).toFixed(1) + ' KB';
return n + ' B';
}
function uaLoadDb() {
api('/admin/site/backup/list', {}).then(function (data) {
var body = $('#ua-db-body');
if (!body) return;
if (data.status !== 200) { body.innerHTML = '| 加载失败 |
'; return; }
var d = data.data || {};
var size = $('#ua-db-size'); if (size) size.textContent = fmtSize(d.db_size || 0);
var hours = $('#ua-db-hours'); if (hours && d.interval_hours != null) hours.value = d.interval_hours;
var baks = d.backups || [];
if (!baks.length) { body.innerHTML = '| 暂无备份 |
'; return; }
body.innerHTML = baks.map(function (b) {
return '| ' + escapeHtml(b.name) + ' | ' + fmtSize(b.size) + ' | ' +
escapeHtml(b.time) + ' | ' +
'下载 ' +
' ' +
' |
';
}).join('');
$$('[data-restore]', body).forEach(function (b) {
b.addEventListener('click', function () {
if (!window.confirm('确定用该备份覆盖当前数据库?')) return;
api('/admin/site/backup/restore', { name: b.getAttribute('data-restore') })
.then(function (d2) {
toast(d2.message || '已恢复', d2.status === 200 ? 'success' : 'error');
if (d2.status === 200) setTimeout(function () { location.reload(); }, 1200);
});
});
});
$$('[data-bdel]', body).forEach(function (b) {
b.addEventListener('click', function () {
if (!window.confirm('确定删除该备份?')) return;
api('/admin/site/backup/delete', { name: b.getAttribute('data-bdel') })
.then(function (d2) { toast(d2.message || '已删除', d2.status === 200 ? 'success' : 'error'); uaLoadDb(); });
});
});
});
}
function uaBackup() {
api('/admin/site/backup/create', {}).then(function (d) {
toast(d.message || '备份完成', d.status === 200 ? 'success' : 'error');
uaLoadDb();
});
}
function uaBackupAuto() {
api('/admin/site/backup/auto', { hours: ($('#ua-db-hours') || {}).value || 0 })
.then(function (d) { toast(d.message || '已保存', d.status === 200 ? 'success' : 'error'); });
}
function uaBackupUpload(file) {
var fd = new FormData();
fd.append('file', file);
fetch('/admin/site/backup/upload', { method: 'POST', body: fd, credentials: 'same-origin' })
.then(function (r) {
if (!r.ok) throw new Error('上传失败(HTTP ' + r.status + ')');
return r.json();
})
.then(function (d) {
toast(d.message || '上传完成', d.status === 200 ? 'success' : 'error');
uaLoadDb();
})
.catch(function (err) {
// 全站唯一绕过统一请求封装的调用此前无失败兜底——
// 网络失败/非 JSON 响应时管理员毫无感知还制造 unhandledrejection 噪音
toast((err && err.message) || '备份上传失败,请检查网络后重试', 'error');
});
}
/* ---------------- 绑定 ---------------- */
function bind() {
bindAuthModal();
var s1 = $('#login-submit'); if (s1) s1.addEventListener('click', doLogin);
var s2 = $('#login-password'); if (s2) s2.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); });
// 回车提交覆盖全部输入框(登录用户名框 / 注册四框)
var s2u = $('#login-username'); if (s2u) s2u.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); });
var s3 = $('#reg-submit'); if (s3) s3.addEventListener('click', doRegister);
var s4 = $('#reg-send-code'); if (s4) s4.addEventListener('click', sendCode);
['#reg-username', '#reg-password', '#reg-email', '#reg-code'].forEach(function (sel) {
var el = $(sel);
if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doRegister(); });
});
// 密码明文切换(登录/注册/找回)
[['#login-pwd-eye', '#login-password'], ['#reg-pwd-eye', '#reg-password'], ['#forgot-pwd-eye', '#forgot-password']].forEach(function (pair) {
var eye = $(pair[0]), input = $(pair[1]);
if (!eye || !input) return;
eye.addEventListener('click', function () {
var show = input.type === 'password';
input.type = show ? 'text' : 'password';
eye.textContent = show ? '隐藏' : '显示';
});
});
// 发码后修改邮箱 → 提前终止倒计时并恢复按钮,提示需重发
var regEmail = $('#reg-email');
if (regEmail) regEmail.addEventListener('input', function () {
var btn = $('#reg-send-code');
if (btn && btn.disabled && /^\d+s$/.test(btn.textContent)) {
clearInterval(window.__regCodeTimer || 0);
btn.disabled = false; btn.textContent = '发送验证码';
toast('邮箱已修改,请重新发送验证码', 'info');
}
});
// 找回视图——发码/提交/回车/改邮箱终止倒计时(对齐注册交互)
var fsc = $('#forgot-send-code'); if (fsc) fsc.addEventListener('click', doForgotSendCode);
var fsub = $('#forgot-submit'); if (fsub) fsub.addEventListener('click', doForgotReset);
['#forgot-email', '#forgot-code', '#forgot-password'].forEach(function (sel) {
var el = $(sel);
if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doForgotReset(); });
});
var forgotEmail = $('#forgot-email');
if (forgotEmail) forgotEmail.addEventListener('input', function () {
var btn = $('#forgot-send-code');
if (btn && btn.disabled && /^\d+s$/.test(btn.textContent)) {
clearInterval(window.__forgotCodeTimer || 0);
btn.disabled = false; btn.textContent = '发送验证码';
toast('邮箱已修改,请重新发送验证码', 'info');
}
});
// 管理员两步验证——发码/验证提交/回车提交
var asc = $('#a2fa-send'); if (asc) asc.addEventListener('click', doA2faSendCode);
var asb = $('#a2fa-submit'); if (asb) asb.addEventListener('click', doA2faVerify);
['#a2fa-email', '#a2fa-code'].forEach(function (sel) {
var el = $(sel);
if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doA2faVerify(); });
});
var s6 = $('#announce-ok'); if (s6) s6.addEventListener('click', function () { closeModal('announce-modal'); });
var ucTab = $('#tab-usercenter');
if (ucTab) ucTab.addEventListener('click', renderUserCenter);
var uaTab = $('#tab-useradmin');
if (uaTab) uaTab.addEventListener('click', uaShowGate);
var av = $('#user-avatar');
if (av) av.addEventListener('click', function () {
if (user && user.role === 'admin') { if (uaTab) uaTab.click(); }
else { var t = $('#tab-usercenter'); if (t) t.click(); }
});
var ucRedeem = $('#uc-card-redeem'); if (ucRedeem) ucRedeem.addEventListener('click', doRedeem);
var ucAnn = $('#uc-ann-view'); if (ucAnn) ucAnn.addEventListener('click', function () { maybeShowAnnouncement(true); }); // 手动查看强制展示
var ucLogout = $('#uc-logout'); if (ucLogout) ucLogout.addEventListener('click', doLogout);
uaBindSide();
var b1 = $('#ua-users-refresh'); if (b1) b1.addEventListener('click', uaLoadUsers);
var b2 = $('#ua-cookie-refresh'); if (b2) b2.addEventListener('click', uaLoadCookieStatus);
var b3 = $('#ua-qr-create'); if (b3) b3.addEventListener('click', uaQrCreate);
var b4 = $('#ua-cookie-delete'); if (b4) b4.addEventListener('click', uaCookieDelete);
var b5 = $('#ua-smtp-save'); if (b5) b5.addEventListener('click', uaSaveSmtp);
var b6 = $('#ua-smtp-test'); if (b6) b6.addEventListener('click', uaTestSmtp);
var b7 = $('#ua-log-refresh'); if (b7) b7.addEventListener('click', function () { uaLoadLogs(); });
var b8 = $('#ua-log-tab-ok'); if (b8) b8.addEventListener('click', function () { uaLogsType = 'success'; uaLogPage = 1; uaLoadLogs(); });
var b9 = $('#ua-log-tab-fail'); if (b9) b9.addEventListener('click', function () { uaLogsType = 'fail'; uaLogPage = 1; uaLoadLogs(); });
var blogPrev = $('#ua-log-prev'); if (blogPrev) blogPrev.addEventListener('click', function () { if (uaLogPage > 1) { uaLogPage--; uaLoadLogs(); } });
// next 补边界(原实现可翻到空页——前端无 pages 时放宽为 999 防呆)
var blogNext = $('#ua-log-next'); if (blogNext) blogNext.addEventListener('click', function () { var maxP = window._uaLogPages || 999; if (uaLogPage < maxP) { uaLogPage++; uaLoadLogs(); } });
var blogClear = $('#ua-log-clear'); if (blogClear) blogClear.addEventListener('click', uaClearLogs);
var b10 = $('#ua-card-gen'); if (b10) b10.addEventListener('click', uaGenCards);
var cardType = $('#ua-card-minutes');
if (cardType) cardType.addEventListener('change', function () {
var custom = $('#ua-card-custom');
if (custom) custom.hidden = cardType.value !== 'custom';
if (cardType.value === 'custom' && custom) custom.focus();
});
var b11 = $('#ua-ann-save'); if (b11) b11.addEventListener('click', uaSaveAnn);
var b12 = $('#ua-db-backup'); if (b12) b12.addEventListener('click', uaBackup);
var b13 = $('#ua-db-auto'); if (b13) b13.addEventListener('click', uaBackupAuto);
var b14 = $('#ua-db-upload');
if (b14) b14.addEventListener('change', function () {
if (b14.files && b14.files[0]) uaBackupUpload(b14.files[0]);
b14.value = '';
});
}
function init() {
bind();
refreshSession();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();