This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
NeteaseUrl/static/site_auth.js
T
snowy-yang 6288a98ecc refactor: 拆分 4 个巨石模块为包,清理过时注释并删除作废文档
为什么:单文件 1100~1900 行的巨石模块让「改一处要通读全文」;同时注释里残留
大量已删台账的编号,以及「Flask/FastAPI 双栈迁移期」这类已失实的架构描述,
会误导后来者按不复存在的架构去改代码。

拆分(对外导入路径不变,行为零变更):
- music_downloader.py  -> music_downloader/(models/downloader/cover/_http/_log/_concurrency)
- auth_system.py       -> auth_system/(db/security/users/credits/ledger/mail/cards/backup...)
- music_api.py         -> music_api/(api/client/crypto/qr/cookies/compat/constants/errors)
- services/runtime.py  -> services/(api_service/_shared/_taskcore/downloads/batch/podcast/
  streaming/tasks);services/runtime.py 保留为兼容门面

兼容性做法:各包用 __init__.py 以 __all__ 原样再导出全部符号,既有调用点
(auth_system.xxx / services.xxx / from music_api import Y)零改动;路径常量改为经包属性
动态读取(auth_system.DB_PATH),否则测试的 monkeypatch 会静默失效并写到生产库。

修复 9 处代码损伤(上一轮注释清理把 `()` 当「空括号残留」删掉所致;用「与拆分前逐字
等价」校验逐一定位,静态检查只会报出其中 4 处):
- core/config.py  ).strip() or '*'
- core/state.py   threading.Lock() x2
- qr_login.py     datetime.now() / .strip()
- music_downloader/downloader.py  .lower()
- services/podcast.py  set() / time.time() x2

清理与删除:
- 删除 部署文档.md / 功能介绍.md / README_私有包警告.txt
- 清理指向已删台账与文档的过时编号注释;把「迁移期 / 两栈 / 双框架 / 已迁移」等
  失实描述改写为现状描述(保留 `<-> Flask /path` 映射表等仍有价值的契约来源说明)
- _mkpkg.py:公开包剔除清单去掉已不存在的文档名;私有包警告改为自包含
- pyproject.toml:补 pythonpath = ["."],使裸跑 pytest 不再 import core 失败

验证:ruff 全通过;pyright 0 errors / 0 warnings(102 文件);pytest 144 passed;
真实 uvicorn 冒烟 6 条路由符合预期且生产 data/ 指纹未变;忽略注释的逐字等价性校验通过。
2026-09-26 12:37:48 +08:00

1435 lines
79 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
* 云村小助手 - 站点账号系统前端逻辑(按次计费版)
* 未经授权不得复制、修改或用于商业用途
* ============================================================ */
(function () {
'use strict';
/* P2-11: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 () {
// F9:默认失败兜底——网络断/超时/非 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() {
/* F-7(第 79 条):与 index.html 的 getDeviceId 统一为同一键/同一值——
注册指纹与下载任务/心跳的设备号不再各走一套;
内存缓存兜底 localStorage 不可写时同一页面内保持稳定(F108 同款)。 */
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; // F-8.2:fail-closed——/auth/me 返回前不显示注册入口(防止未配置 SMTP 的窗口期误开)
var registerCredits = 3; // F-6.1:注册赠送次数由 /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;
});
// F-8.1:两个弹窗(登录/注册)里的试用提示都要受管——原 querySelector 只命中第一个
$$('.auth-trial-box').forEach(function (box) { box.hidden = !showRegister; });
$$('.reg-gift-num').forEach(function (el) { el.textContent = registerCredits; }); // F-6.1
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;
// F-6.1:注册赠送次数动态下发(/auth/me),文案不再硬编码
if (typeof d.register_credits === 'number') registerCredits = d.register_credits;
renderAuthArea();
renderUserCenter();
maybeShowAnnouncement();
// A1:确认登录后再恢复下载任务(游客调任务接口会 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;
}
// F53:会话失效时清掉内存里的旧用户状态再引导登录
//(原实现保留旧 user,页面继续显示"已登录"却处处 401)
user = null;
durationText = '';
renderAuthArea();
renderUserCenter();
authToastOnce(msg || '登录已失效,请重新登录');
// 第 87 条:登录弹窗已打开且正处于找回/两步验证视图时,不复位视图——
// 未登录期间后台任务轮询/心跳会持续吃 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();
}
// 第 98 条:会话失效弹登录窗前,先关掉可能叠开的注册弹窗(双遮罩错乱),
// 已 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 =
'<div class="modal" style="width:380px;">' +
' <div class="modal-header"><div class="modal-title">⏳ 下载次数已用尽</div></div>' +
' <div class="modal-body" style="font-size:13px;line-height:1.9;">' +
' 您的下载次数已用完。解析与在线试听免费,<b>下载需要消耗次数</b>(一首歌 = 1 次,歌单打包按歌曲数量扣减)。<br>' +
' <span style="opacity:.7;">请在个人中心充值卡密后继续下载。</span>' +
' </div>' +
' <div style="display:flex;gap:10px;padding:0 22px 20px;">' +
' <button class="btn btn-primary" id="site-expired-go" type="button" style="flex:1;">去充值</button>' +
' <button class="btn" id="site-expired-close" type="button" style="flex:1;">关闭</button>' +
' </div>' +
'</div>';
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();
// A1:登录成功后立即恢复本设备的进行中任务
if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') {
try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {}
}
}
/* ---------------- 登录 / 注册弹窗 ---------------- */
// 第 81 条:注册唯一入口——导航按钮与登录弹窗两处链接(全站仅有的 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(); // 第 87 条:关闭侧复位
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(); // 第 87 条:关闭侧复位
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'); });
// 第 86 条:找回密码入口/返回(F5.1 只交付 DOM 漏了绑定)
var gf = $('#goto-forgot');
if (gf) gf.addEventListener('click', showForgotView);
var fbl = $('#forgot-back-login');
if (fbl) fbl.addEventListener('click', hideForgotView);
// 第 86 条:管理员两步验证视图的返回链接
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 || '登录失败');
// 第 86 条:管理员密码验证通过——转两步验证视图(服务端未建会话)
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}$/;
// F5.1:注册弹窗内常驻错误行(长文案 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 = '发送验证码';
});
}
/* ---------------- 找回密码 ---------------- */
// 第 84 条:F5.1 只交付了找回视图 DOM 未实现 JS——此处补齐全链路。
// 后端 /auth/forgot_password(第 79 条)已就绪:验证码原子消费、防枚举
// 统一文案、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 = '重置密码'; });
}
/* ---------------- 管理员两步验证(第 86 条) ----------------
* 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 || '-';
// 第 87 条:清掉上次会话残留的输入(错码/旧邮箱不再被原样重复提交)
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();
}
// 第 87 条:关闭侧统一复位——登录弹窗的 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) {
// 第 87 条:先复位视图再提示——错误行随视图隐藏,此处 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) {
// 第 87 条:先复位视图再提示(同上,错误行随视图隐藏)
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();
// 第 81 条:提交侧兜底闸(入口已统一走 openRegisterFlow,此处防任何程序化
// 打开路径绕过)。fail-closed:协议闸缺失视为未同意。注册语境只查协议同意
// (功能性 Cookie 不拦注册,拦解析/下载时另有提示);未同意时闸内已自动
// 弹「隐私与协议设置」注册模式并 toast,此处不再重复提示
if (typeof window.SITE_COOKIE_GATE !== 'function') {
toast('页面协议组件未就绪,请刷新页面后重试', 'error');
return;
}
if (!window.SITE_COOKIE_GATE('register')) return;
// F1.1:前端即时校验(口径与后端一致,省一次网络往返)
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(); // 第 83 条:同步个人中心 gate/content 显隐,与其他登出路径一致
if (window.resetAuthViews) window.resetAuthViews(); // 第 87 条:登出复位登录弹窗视图
closeModal('register-modal'); closeModal('login-modal');
// 第 102 条(U15 拍板):退出连带清除个人网易云登录(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 '<div style="margin-bottom:10px;"><b>v' + v.v + '</b> <span style="opacity:.6;font-size:12px;">' +
v.date + '</span><div style="font-size:12px;opacity:.85;line-height:1.7;">' +
v.items.map(function (i) { return '· ' + i; }).join('<br>') + '</div></div>';
}).join('') + '<div style="opacity:.6;font-size:12px;">当前版本 v' + SITE_VERSION +
'</div>';
}
}
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;
// F69:同步更新内存余额(批量下载预检读的是 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) {
// F81:手动"查看公告"(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('<br>');
// 第 98 条:首访 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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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) {
// 第 97 条(P3):侧栏项补键盘语义(F109 同型——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);
// 第 97 条(P3):返回/退出两个无 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 密钥(第 78 条) ---- */
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 =
'<div class="modal" style="width:400px;">' +
' <div class="modal-header"><div class="modal-title">➕ 添加用户</div></div>' +
' <div class="modal-body">' +
' <input class="input" id="cnu-username" placeholder="用户名(3-20位)" style="width:100%;margin-bottom:10px;"/>' +
' <input class="input" id="cnu-password" placeholder="初始密码(至少8位)" style="width:100%;margin-bottom:10px;"/>' +
' <input class="input" id="cnu-email" placeholder="邮箱(可选)" style="width:100%;margin-bottom:10px;"/>' +
' <input class="input" id="cnu-count" type="number" min="0" value="10" placeholder="下载次数" style="width:100%;"/>' +
' </div>' +
' <div style="display:flex;gap:10px;padding:0 22px 20px;">' +
' <button class="btn btn-primary" id="cnu-submit" type="button" style="flex:1;">创 建</button>' +
' <button class="btn" id="cnu-cancel" type="button" style="flex:1;">取消</button>' +
' </div>' +
'</div>';
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 = '<tr><td colspan="8">' + escapeHtml(data.message || '加载失败') + '</td></tr>'; return; }
var users = (data.data && data.data.users) || [];
var cnt = $('#ua-users-count');
if (cnt) cnt.textContent = users.length;
if (!users.length) { body.innerHTML = '<tr><td colspan="8">暂无用户</td></tr>'; return; }
body.innerHTML = users.map(function (u) {
var isAdmin = u.role === 'admin';
var disabled = (u.status === 0);
var banned = (u.status === 2);
// 第 97 条(P3):数值字段同过转义(纵深防御——后端口径若变为字符串
// 不再形成管理后台存储型 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(' · ') || '-'; // R5-79:IP 优先显示
if (u.last_province) dev += ' · ' + u.last_province;
return '<tr' + ((disabled || banned) ? ' style="opacity:.55;"' : '') + '>' +
'<td>' + uid + '</td>' +
'<td>' + escapeHtml(u.username) +
(banned ? ' <span class="tag-pill" style="background:#8a1f1f;color:#fff;font-size:11px;">已封禁</span>' : '') +
(disabled ? ' <span class="tag-pill" style="background:var(--color-error,#e0483f);color:#fff;font-size:11px;">已禁用</span>' : '') + '</td>' +
'<td>' + escapeHtml(u.email || '-') + '</td>' +
'<td>' + (isAdmin ? '管理员' : '用户') + '</td>' +
'<td>' + cred + '</td>' +
'<td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + escapeHtml(dev) + '">' + escapeHtml(dev) + '</td>' +
'<td><button class="btn btn-sm" data-ucookie="' + escapeHtml(u.username) + '">' + (u.has_netease_cookie ? '查看' : '无') + '</button></td>' +
'<td>' +
'<button class="btn btn-sm" data-uedit="' + uid + '" data-cur="' + dur + '">改次数</button> ' +
(isAdmin ? '<button class="btn btn-sm" data-uself="' + uid + '">改自己密码</button>' :
'<button class="btn btn-sm" data-upwd="' + uid + '" data-name="' + escapeHtml(u.username) + '">重置密码</button> ' +
'<button class="btn btn-sm" data-utoggle="' + uid + '">' + (disabled ? '启用' : '禁用') + '</button> ' +
'<button class="btn btn-sm" data-uban="' + uid + '" data-name="' + escapeHtml(u.username) + '">' + (banned ? '解封' : '封禁') + '</button> ' +
'<button class="btn btn-sm" data-udel="' + uid + '" data-name="' + escapeHtml(u.username) + '" style="color:var(--color-error);">删除</button>') +
'</td></tr>';
}).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(); });
});
});
// T3:管理员自助改密(需验证旧密码;改完其他设备全部下线)
$$('[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; // T2:轮询代次令牌(删除 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 = '<img src="' + uri + '" style="width:180px;height:180px;" alt="二维码"/>';
}
if (tip) tip.textContent = '请使用网易云音乐手机 APP 扫码';
if (uaQrTimer) clearInterval(uaQrTimer);
uaQrGen++; // T2:新轮询代次
uaQrTimer = setInterval(uaQrPoll, 3000);
});
}
function uaQrPoll() {
if (!uaUnikey) { if (uaQrTimer) clearInterval(uaQrTimer); return; }
var myGen = uaQrGen; // T2:本次轮询所属代次
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) {
// T2:作废在途扫码轮询——迟到的 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; // P3-43: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 '<div>[' + escapeHtml(l.time) + '] ' + escapeHtml(l.username || '-') +
' · ' + escapeHtml(l.action) + ' · IP ' + escapeHtml(l.ip) +
(l.detail ? ' · ' + escapeHtml(l.detail) : '') + '</div>';
}).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);
});
}
// 第 81 条:卡密管理操作列(禁用/启用/删除)+ 分页(F85 后端已支持,
// 不分页则超出最新 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 = '<tr><td colspan="8">加载失败</td></tr>'; return; }
var cards = (data.data && data.data.cards) || [];
_uaCardTotal = (data.data && data.data.total) || 0;
if (!cards.length) {
body.innerHTML = '<tr><td colspan="8">暂无卡密</td></tr>';
_uaRenderCardPager();
return;
}
body.innerHTML = cards.map(function (c) {
var st, act;
if (c.used_by) {
st = '已使用';
act = '<span class="hint">—</span>'; // 兑换凭证保留,不给删除/禁用
} else if (c.disabled) {
st = '已禁用';
act = '<button class="btn btn-sm" data-card-act="enable" data-card-code="' + escapeHtml(c.code) + '" type="button">启用</button> ' +
'<button class="btn btn-danger btn-sm" data-card-act="delete" data-card-code="' + escapeHtml(c.code) + '" type="button">删除</button>';
} else {
// 过期判定用服务端下发的 expired(客户端时钟偏差会误标)
st = c.expired ? '已过期' : '未使用';
act = '<button class="btn btn-sm" data-card-act="disable" data-card-code="' + escapeHtml(c.code) + '" type="button">禁用</button> ' +
'<button class="btn btn-danger btn-sm" data-card-act="delete" data-card-code="' + escapeHtml(c.code) + '" type="button">删除</button>';
}
return '<tr><td>' + escapeHtml(c.code) + '</td><td>' + escapeHtml(String(c.minutes)) + ' 次</td><td>' + st + '</td><td>' + escapeHtml(c.used_by || '-') + '</td><td>' +
escapeHtml(c.note || '-') + '</td><td>' +
(c.expire_at ? new Date(c.expire_at * 1000).toLocaleDateString() : '永久') + '</td><td>' +
escapeHtml(c.created_at ? new Date(c.created_at * 1000).toLocaleDateString() : '-') + '</td><td>' + act + '</td></tr>';
}).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 = '<tr><td colspan="4">加载失败</td></tr>'; 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 = '<tr><td colspan="4">暂无备份</td></tr>'; return; }
body.innerHTML = baks.map(function (b) {
return '<tr><td>' + escapeHtml(b.name) + '</td><td>' + fmtSize(b.size) + '</td><td>' +
escapeHtml(b.time) + '</td><td>' +
'<a class="btn btn-sm" href="/admin/site/backup/download?name=' + encodeURIComponent(b.name) + '">下载</a> ' +
'<button class="btn btn-sm" data-restore="' + escapeHtml(b.name) + '">恢复</button> ' +
'<button class="btn btn-sm" data-bdel="' + escapeHtml(b.name) + '" style="color:var(--color-error);">删除</button></td></tr>';
}).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) {
// 第 97 条(P2):全站唯一绕过统一请求封装的调用此前无失败兜底——
// 网络失败/非 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(); });
// F-3.1:回车提交覆盖全部输入框(登录用户名框 / 注册四框)
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(); });
});
// F-4.1:密码明文切换(登录/注册/找回)
[['#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 ? '隐藏' : '显示';
});
});
// F-2.1:发码后修改邮箱 → 提前终止倒计时并恢复按钮,提示需重发
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');
}
});
// 第 84 条:找回视图——发码/提交/回车/改邮箱终止倒计时(对齐注册交互)
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');
}
});
// 第 86 条:管理员两步验证——发码/验证提交/回车提交
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); }); // F81:手动查看强制展示
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(); } });
// P3-43: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();
}
})();