/* =========================================================================
路由拓扑 · 管理控制台(独立页面)
页面地址:web/admin.html,资源相对 web/,API 相对站点根(../index.php)
========================================================================= */
(function () {
'use strict';
/* ============================================================
基础工具
============================================================ */
function $(id) { return document.getElementById(id); }
function esc(s) {
return String(s === null || s === undefined ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
function qs(obj) {
const parts = [];
for (const k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k) && obj[k] !== '' && obj[k] !== null && obj[k] !== undefined) {
parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]));
}
}
return parts.length ? ('?' + parts.join('&')) : '';
}
function fmtTime(iso) {
if (!iso) { return '—'; }
const d = new Date(iso);
if (isNaN(d.getTime())) { return String(iso); }
const p = function (n) { return n < 10 ? '0' + n : '' + n; };
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) +
' ' + p(d.getHours()) + ':' + p(d.getMinutes());
}
function debounce(fn, ms) {
let t = null;
return function () {
const self = this, args = arguments;
clearTimeout(t);
t = setTimeout(function () { fn.apply(self, args); }, ms || 260);
};
}
function ts() {
const d = new Date(), p = function (n) { return n < 10 ? '0' + n : '' + n; };
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '-' +
p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
}
function downloadJson(obj, filename) {
const str = JSON.stringify(obj, null, 2);
const blob = new Blob([str], { type: 'application/json;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 8000);
}
/* ============================================================
API 客户端(独立页,base = ../index.php)
============================================================ */
const API = {
req: function (method, path, body) {
const opt = { method: method, credentials: 'same-origin', headers: {} };
if (body !== undefined) {
opt.headers['Content-Type'] = 'application/json';
opt.body = JSON.stringify(body);
}
return fetch('../index.php?r=' + encodeURIComponent('/api' + path), opt).then(function (r) {
return r.text().then(function (txt) {
let data = null;
try { data = txt ? JSON.parse(txt) : null; } catch (e) { data = null; }
if (!r.ok || (data && data.ok === false)) {
const msg = (data && data.error) ? data.error : ('请求失败 (' + r.status + ')');
const err = new Error(msg);
err.status = r.status;
throw err;
}
return data || {};
});
});
},
me: function () { return this.req('GET', '/me'); },
dashboard: function () { return this.req('GET', '/dashboard'); },
logs: function (p) { return this.req('GET', '/logs' + qs(p)); },
listUsers: function (p) { return this.req('GET', '/users' + qs(p)); },
createUser: function (p) { return this.req('POST', '/users', p); },
updateUser: function (id, p) { return this.req('PUT', '/users/' + encodeURIComponent(id), p); },
resetUserPassword: function (id, p) { return this.req('PUT', '/users/' + encodeURIComponent(id) + '/password', p); },
deleteUser: function (id) { return this.req('DELETE', '/users/' + encodeURIComponent(id)); },
adminTopologies: function (p) { return this.req('GET', '/admin/topologies' + qs(p)); },
exportTopos: function (ids) {
const q = (ids && ids.length) ? ('?ids=' + encodeURIComponent(ids.join(','))) : '';
return this.req('GET', '/admin/topologies/export' + q);
},
siteSettings: function () { return this.req('GET', '/admin/settings'); },
saveSiteSettings: function (p) { return this.req('PUT', '/admin/settings', p); },
setVisibility: function (id, vis) { return this.req('PUT', '/topologies/' + encodeURIComponent(id) + '/permission', { visibility: vis }); },
renameTopo: function (id, name) { return this.req('PUT', '/topologies/' + encodeURIComponent(id) + '/rename', { name: name }); },
deleteTopo: function (id) { return this.req('DELETE', '/topologies/' + encodeURIComponent(id)); }
};
/* ============================================================
提示条 / 站内弹窗
============================================================ */
let toastTimer = null;
function toast(msg, type) {
const el = $('toast');
el.textContent = msg;
el.className = 'toast' + (type ? (' ' + type) : '');
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { el.classList.remove('show'); }, 2600);
}
function openOverlay(id) { const o = $(id); if (o) { o.hidden = false; } }
function closeOverlay(id) { const o = $(id); if (o) { o.hidden = true; } }
let _dlgResolve = null;
let _dlgInput = false;
function _dlgFinish(result) {
closeOverlay('confirmOverlay');
const r = _dlgResolve;
_dlgResolve = null;
if (r) { r(result); }
}
function _dlgConfirmOk() {
if (_dlgInput) {
const v = $('confirmField').value.trim();
if (!v) { $('confirmErr').textContent = '请输入内容'; return; }
_dlgFinish(v);
} else {
_dlgFinish(true);
}
}
function _uiDialog(o) {
return new Promise(function (resolve) {
_dlgResolve = resolve;
_dlgInput = !!o.input;
$('confirmTitle').textContent = o.title || '请确认';
const msg = $('confirmMsg');
msg.textContent = o.message || '';
msg.hidden = !o.message;
const okBtn = $('confirmOk');
okBtn.textContent = o.okText || '确定';
okBtn.className = 'btn ' + (o.danger ? 'danger' : 'primary');
const wrap = $('confirmFieldWrap');
const field = $('confirmField');
if (o.input) {
wrap.hidden = false;
$('confirmFieldLabel').textContent = o.input.label || '';
field.type = o.input.type || 'text';
field.placeholder = o.input.placeholder || '';
field.value = o.input.value || '';
} else {
wrap.hidden = true;
}
$('confirmErr').textContent = '';
openOverlay('confirmOverlay');
setTimeout(function () {
if (o.input) { field.focus(); field.select(); } else { okBtn.focus(); }
}, 30);
});
}
function uiConfirm(message, opts) {
opts = opts || {};
return _uiDialog({
title: opts.title || '请确认', message: message,
okText: opts.okText || '确定', danger: !!opts.danger
});
}
function uiPrompt(label, opts) {
opts = opts || {};
return _uiDialog({
title: opts.title || '请输入', message: opts.message || '',
okText: opts.okText || '确定',
input: { label: label, placeholder: opts.placeholder || '', type: opts.type || 'text', value: opts.value || '' }
});
}
/* ============================================================
渲染小工具
============================================================ */
const ACTION_LABEL = {
login: '登录', logout: '退出', register: '注册', password_change: '修改密码',
user_create: '创建用户', user_update: '更新用户', user_delete: '删除用户', user_reset_password: '重置用户密码',
topo_create: '创建拓扑', topo_delete: '删除拓扑', topo_permission: '设置可见性', topo_rename: '重命名拓扑',
site_settings: '网站设置'
};
function actionLabel(a) { return ACTION_LABEL[a] || a || '—'; }
function statCard(label, num, cls) {
return '
' +
'
' + esc(num === null || num === undefined ? 0 : num) + '
' +
'
' + esc(label) + '
';
}
function miniRow(time, content) {
return '' + esc(time) + '' + content + '
';
}
function emptyRow(msg) { return '' + esc(msg) + '
'; }
function renderPager(containerId, total, page, pageSize, onGo) {
const totalPages = Math.max(1, Math.ceil((total || 0) / pageSize));
const el = $(containerId);
el.innerHTML =
'共 ' + esc(total || 0) + ' 条 · 第 ' + esc(page) + ' / ' + esc(totalPages) + ' 页' +
'' +
'';
const prev = el.querySelector('[data-go="prev"]');
const next = el.querySelector('[data-go="next"]');
if (prev) { prev.addEventListener('click', function () { if (page > 1) { onGo(page - 1); } }); }
if (next) { next.addEventListener('click', function () { if (page < totalPages) { onGo(page + 1); } }); }
}
/* ============================================================
状态
============================================================ */
const state = {
view: 'overview',
currentUser: null,
users: { page: 1, pageSize: 10, q: '', total: 0 },
topos: { page: 1, pageSize: 10, q: '', owner: '', total: 0 },
logs: { page: 1, pageSize: 12, q: '', total: 0 }
};
function handleErr(err) {
toast((err && err.message) || '操作失败', 'err');
if (err && (err.status === 401 || err.status === 403)) {
setTimeout(function () { window.location.href = '../index.php'; }, 1200);
}
}
/* ============================================================
视图切换
============================================================ */
function switchView(name) {
state.view = name;
['overview', 'users', 'topo', 'settings', 'logs'].forEach(function (v) {
const el = $('view-' + v);
if (el) { el.hidden = (v !== name); }
});
Array.prototype.forEach.call(document.querySelectorAll('.cnav-item'), function (it) {
it.classList.toggle('active', it.getAttribute('data-view') === name);
});
if (name === 'overview') { loadOverview(); }
else if (name === 'users') { loadUsers(); }
else if (name === 'topo') { loadTopoOwnerOptions(); loadTopoAdmin(); }
else if (name === 'settings') { loadSettings(); }
else if (name === 'logs') { loadLogs(); }
}
/* ============================================================
概览
============================================================ */
function loadOverview() {
API.dashboard().then(function (d) {
const s = d.stats || {};
$('statCards').innerHTML = [
statCard('用户总数', s.userCount, ''),
statCard('管理员', s.adminCount, 'accent'),
statCard('已禁用', s.disabledCount, 'amber'),
statCard('拓扑总数', s.topoCount, ''),
statCard('公开拓扑', s.publicCount, 'green'),
statCard('今日新增拓扑', s.todayTopoCount, ''),
statCard('今日登录', s.todayLoginCount, 'accent'),
statCard('今日操作', s.todayLogCount, '')
].join('');
const logs = d.recentLogs || [];
$('recentLogs').innerHTML = logs.length ? logs.map(function (l) {
return miniRow(fmtTime(l.createdAt),
esc((l.username || '系统') + ' · ' + (l.detail || actionLabel(l.action))));
}).join('') : emptyRow('暂无记录');
const topos = d.recentTopos || [];
$('recentTopos').innerHTML = topos.length ? topos.map(function (t) {
return miniRow(fmtTime(t.updatedAt), esc(t.name + ' · ' + (t.ownerName || '未知')));
}).join('') : emptyRow('暂无拓扑');
}).catch(handleErr);
}
/* ============================================================
用户管理
============================================================ */
function loadUsers() {
const q = state.users;
API.listUsers({ page: q.page, pageSize: q.pageSize, q: q.q }).then(function (d) {
const rows = d.users || [];
$('userRows').innerHTML = rows.length ? rows.map(function (u) {
const roleBadge = (u.role === 'admin')
? '管理员'
: '普通用户';
const status = u.disabled
? '已禁用'
: '正常';
const pwdFlag = u.mustChangePassword ? ' 待改密' : '';
return '' +
'| ' + esc(u.username) + ' | ' +
'' + roleBadge + ' | ' +
'' + status + pwdFlag + ' | ' +
'' + esc(u.loginCount) + ' | ' +
'' + esc(fmtTime(u.lastLoginAt)) + ' | ' +
'' + esc(fmtTime(u.createdAt)) + ' | ' +
'' + userActions(u) + ' | ' +
'
';
}).join('') : '| 暂无用户 |
';
renderPager('userPager', d.total, q.page, q.pageSize, function (p) { q.page = p; loadUsers(); });
}).catch(handleErr);
}
function userActions(u) {
const isSelf = state.currentUser && state.currentUser.id === u.id;
const btns = [];
btns.push('');
btns.push('');
if (!isSelf) {
btns.push('');
btns.push('');
}
return btns.join('');
}
function onUserRowClick(e) {
const b = e.target.closest('button[data-act]');
if (!b) { return; }
const act = b.getAttribute('data-act');
const id = b.getAttribute('data-id');
if (act === 'pwd') {
openResetDialog(id, b.getAttribute('data-name'));
} else if (act === 'role') {
const role = b.getAttribute('data-role');
const label = (role === 'admin' ? '管理员' : '普通用户');
uiConfirm('确认将该用户角色改为「' + label + '」?', { title: '修改角色' }).then(function (ok) {
if (!ok) { return; }
API.updateUser(id, { role: role }).then(function () {
toast('角色已更新', 'ok'); loadUsers();
}).catch(handleErr);
});
} else if (act === 'toggle') {
const disabled = b.getAttribute('data-disabled') === '1';
uiConfirm(disabled ? '确认禁用该账号?禁用后该用户将无法登录。' : '确认启用该账号?',
{ title: disabled ? '禁用账号' : '启用账号', danger: disabled }).then(function (ok) {
if (!ok) { return; }
API.updateUser(id, { disabled: disabled }).then(function () {
toast(disabled ? '已禁用' : '已启用', 'ok'); loadUsers();
}).catch(handleErr);
});
} else if (act === 'del') {
uiConfirm('确认删除用户「' + b.getAttribute('data-name') + '」?该操作不可恢复。',
{ title: '删除用户', danger: true, okText: '删除' }).then(function (ok) {
if (!ok) { return; }
API.deleteUser(id).then(function () {
toast('用户已删除', 'ok'); loadUsers();
}).catch(handleErr);
});
}
}
function openUserDialog() {
$('userDlgTitle').textContent = '新建用户';
$('uName').value = '';
$('uPass').value = '';
$('uRole').value = 'user';
$('userErr').textContent = '';
openOverlay('userOverlay');
setTimeout(function () { $('uName').focus(); }, 30);
}
function submitUserDialog() {
const name = $('uName').value.trim();
const pass = $('uPass').value;
const role = $('uRole').value;
$('userErr').textContent = '';
if (!name) { $('userErr').textContent = '请输入用户名'; return; }
if (!pass) { $('userErr').textContent = '请输入密码'; return; }
$('btnSaveUser').disabled = true;
API.createUser({ username: name, password: pass, role: role }).then(function () {
$('btnSaveUser').disabled = false;
closeOverlay('userOverlay');
toast('用户已创建', 'ok');
state.users.page = 1;
loadUsers();
}).catch(function (err) {
$('btnSaveUser').disabled = false;
$('userErr').textContent = (err && err.message) || '创建失败';
});
}
/* ============================================================
重置用户密码(需先验证管理员本人密码)
============================================================ */
let resetUserId = null;
function openResetDialog(id, username) {
resetUserId = id;
$('resetTip').textContent = '为用户「' + username + '」设置新密码,该用户下次登录需再次修改。';
$('resetAdminPw').value = '';
$('resetNewPw').value = '';
$('resetNewPw2').value = '';
$('resetShow').checked = false;
['resetAdminPw', 'resetNewPw', 'resetNewPw2'].forEach(function (i) { $(i).type = 'password'; });
$('resetErr').textContent = '';
openOverlay('resetOverlay');
setTimeout(function () { $('resetAdminPw').focus(); }, 30);
}
function submitResetDialog() {
if (!resetUserId) { return; }
const adminPw = $('resetAdminPw').value;
const np = $('resetNewPw').value;
const np2 = $('resetNewPw2').value;
const err = $('resetErr');
err.textContent = '';
if (!adminPw) { err.textContent = '请输入您(管理员)的密码'; return; }
if (!np) { err.textContent = '请输入新密码'; return; }
if (np !== np2) { err.textContent = '两次输入的新密码不一致'; return; }
$('btnDoReset').disabled = true;
API.resetUserPassword(resetUserId, { adminPassword: adminPw, newPassword: np }).then(function () {
$('btnDoReset').disabled = false;
closeOverlay('resetOverlay');
toast('密码已重置', 'ok');
loadUsers();
}).catch(function (e) {
$('btnDoReset').disabled = false;
err.textContent = (e && e.message) || '重置失败';
});
}
/* ============================================================
拓扑管理
============================================================ */
function loadTopoOwnerOptions() {
API.listUsers({ page: 1, pageSize: 100 }).then(function (d) {
const sel = $('topoOwner');
const cur = state.topos.owner;
const opts = [''];
(d.users || []).forEach(function (u) {
opts.push('');
});
sel.innerHTML = opts.join('');
sel.value = cur;
}).catch(function () { /* 忽略:仅用于筛选下拉 */ });
}
function loadTopoAdmin() {
const q = state.topos;
API.adminTopologies({ page: q.page, pageSize: q.pageSize, q: q.q, owner: q.owner }).then(function (d) {
const rows = d.topologies || [];
const allSel = $('topoSelectAll');
if (allSel) { allSel.checked = false; }
$('topoRows').innerHTML = rows.length ? rows.map(function (t) {
const vis = (t.visibility === 'public')
? '公开'
: '私有';
return '' +
' | ' +
'' + esc(t.name) + ' | ' +
'' + esc(t.ownerName || '—') + ' | ' +
'' + vis + ' | ' +
'' + esc(t.nodeCount) + ' / ' + esc(t.edgeCount) + ' | ' +
'' + esc(fmtTime(t.updatedAt)) + ' | ' +
'' +
'' +
'' +
'' +
' | ' +
'
';
}).join('') : '| 暂无拓扑 |
';
renderPager('topoPager', d.total, q.page, q.pageSize, function (p) { q.page = p; loadTopoAdmin(); });
}).catch(handleErr);
}
function onTopoRowClick(e) {
const b = e.target.closest('button[data-act]');
if (!b) { return; }
const act = b.getAttribute('data-act');
const id = b.getAttribute('data-id');
if (act === 'rename') {
const curName = b.getAttribute('data-name');
uiPrompt('新名称', {
title: '重命名拓扑',
message: '为拓扑「' + curName + '」设置新名称(最多 60 个字符)。',
placeholder: '例如:某内网横向拓扑',
value: curName,
okText: '保存'
}).then(function (v) {
if (v === null) { return; }
v = String(v).trim();
if (!v || v === curName) { return; }
API.renameTopo(id, v).then(function () {
toast('已重命名', 'ok'); loadTopoAdmin();
}).catch(handleErr);
});
} else if (act === 'vis') {
const vis = b.getAttribute('data-vis');
const label = (vis === 'public' ? '公开' : '私有');
uiConfirm('确认将该拓扑设为「' + label + '」?', { title: '修改可见性' }).then(function (ok) {
if (!ok) { return; }
API.setVisibility(id, vis).then(function () {
toast('可见性已更新', 'ok'); loadTopoAdmin();
}).catch(handleErr);
});
} else if (act === 'del') {
uiConfirm('确认删除拓扑「' + b.getAttribute('data-name') + '」?该操作不可恢复。',
{ title: '删除拓扑', danger: true, okText: '删除' }).then(function (ok) {
if (!ok) { return; }
API.deleteTopo(id).then(function () {
toast('拓扑已删除', 'ok'); loadTopoAdmin();
}).catch(handleErr);
});
}
}
/* ============================================================
拓扑批量操作 / 导出
============================================================ */
function selectedTopoIds() {
return Array.prototype.map.call(
document.querySelectorAll('#topoRows .topo-chk:checked'),
function (c) { return c.getAttribute('data-id'); }
);
}
function exportTopos(all) {
const ids = all ? [] : selectedTopoIds();
if (!all && !ids.length) { toast('请先勾选要导出的拓扑', 'warn'); return; }
API.exportTopos(ids).then(function (d) {
const list = d.topologies || [];
if (!list.length) { toast('没有可导出的拓扑', 'warn'); return; }
const payload = {
version: 1,
type: 'route-topology-bundle',
exportedAt: d.exportedAt || new Date().toISOString(),
count: list.length,
topologies: list.map(function (t) {
return {
name: t.name, ownerName: t.ownerName, visibility: t.visibility,
nodeCount: t.nodeCount, edgeCount: t.edgeCount, updatedAt: t.updatedAt,
nodes: t.nodes || [], edges: t.edges || []
};
})
};
downloadJson(payload, 'route-topologies-' + ts() + '.json');
toast('已导出 ' + list.length + ' 个拓扑', 'ok');
}).catch(handleErr);
}
function deleteSelectedTopos() {
const ids = selectedTopoIds();
if (!ids.length) { toast('请先勾选要删除的拓扑', 'warn'); return; }
uiConfirm('确认删除所选 ' + ids.length + ' 个拓扑?该操作不可恢复。',
{ title: '批量删除', danger: true, okText: '删除' }).then(function (ok) {
if (!ok) { return; }
Promise.all(ids.map(function (id) { return API.deleteTopo(id); })).then(function () {
toast('已删除 ' + ids.length + ' 个拓扑', 'ok');
loadTopoAdmin();
}).catch(handleErr);
});
}
/* ============================================================
网站设置
============================================================ */
let logoValue = ''; // 表单中的 Logo(data URI 或 '')
function renderLogoPreview(logo) {
const box = $('logoPreview');
if (!box) { return; }
if (logo) {
box.innerHTML = '';
const img = document.createElement('img');
img.src = logo;
img.alt = 'logo';
box.appendChild(img);
box.classList.add('has-img');
} else {
box.textContent = 'R';
box.classList.remove('has-img');
}
}
function applyAdminBrand(logo) {
const el = document.querySelector('.console-brand .logo');
if (!el) { return; }
if (logo) {
el.innerHTML = '';
const img = document.createElement('img');
img.src = logo;
img.alt = '';
el.appendChild(img);
el.classList.add('has-img');
} else {
el.textContent = 'R';
el.classList.remove('has-img');
}
}
function loadSettings() {
API.siteSettings().then(function (d) {
const s = d.settings || {};
$('setSiteName').value = s.site_name || '';
$('setAllowReg').checked = !!s.allow_registration;
$('setTtl').value = (s.session_ttl_days || 30);
logoValue = s.site_logo || '';
renderLogoPreview(logoValue);
applyAdminBrand(logoValue);
}).catch(handleErr);
}
function saveSettings() {
const name = $('setSiteName').value.trim();
if (!name) { toast('请输入网站名称', 'warn'); return; }
const ttl = parseInt($('setTtl').value, 10);
if (!ttl || ttl < 1) { toast('会话有效期需为不小于 1 的整数', 'warn'); return; }
$('btnSaveSettings').disabled = true;
API.saveSiteSettings({
site_name: name,
site_logo: logoValue,
allow_registration: $('setAllowReg').checked,
session_ttl_days: ttl
}).then(function (d) {
$('btnSaveSettings').disabled = false;
const s = d.settings || {};
logoValue = s.site_logo || '';
renderLogoPreview(logoValue);
applyAdminBrand(logoValue);
$('setSiteName').value = s.site_name || name;
toast('设置已保存', 'ok');
}).catch(function (err) {
$('btnSaveSettings').disabled = false;
handleErr(err);
});
}
function onPickLogo() {
const input = $('logoFile');
const f = input.files && input.files[0];
if (!f) { return; }
if (f.size > 300 * 1024) {
toast('图片过大,请控制在 300KB 以内', 'warn');
input.value = '';
return;
}
const reader = new FileReader();
reader.onload = function () { logoValue = String(reader.result || ''); renderLogoPreview(logoValue); };
reader.onerror = function () { toast('读取图片失败', 'err'); };
reader.readAsDataURL(f);
input.value = '';
}
/* ============================================================
操作日志
============================================================ */
function loadLogs() {
const q = state.logs;
API.logs({ page: q.page, pageSize: q.pageSize, q: q.q }).then(function (d) {
const rows = d.logs || [];
$('logRows').innerHTML = rows.length ? rows.map(function (l) {
return '' +
'| ' + esc(fmtTime(l.createdAt)) + ' | ' +
'' + esc(l.username || '—') + ' | ' +
'' + esc(actionLabel(l.action)) + ' | ' +
'' + esc(l.target || '—') + ' | ' +
'' + esc(l.detail || '—') + ' | ' +
'' + esc(l.ip || '—') + ' | ' +
'
';
}).join('') : '| 暂无日志 |
';
renderPager('logPager', d.total, q.page, q.pageSize, function (p) { q.page = p; loadLogs(); });
}).catch(handleErr);
}
/* ============================================================
事件绑定
============================================================ */
function bindEvents() {
Array.prototype.forEach.call(document.querySelectorAll('.cnav-item'), function (it) {
it.addEventListener('click', function () { switchView(it.getAttribute('data-view')); });
});
$('btnCollapseNav').addEventListener('click', function () {
$('console').classList.toggle('nav-collapsed');
});
$('btnCloseAdmin').addEventListener('click', function () {
window.location.href = '../index.php';
});
$('userSearch').addEventListener('input', debounce(function () {
state.users.q = this.value.trim(); state.users.page = 1; loadUsers();
}));
$('topoSearch').addEventListener('input', debounce(function () {
state.topos.q = this.value.trim(); state.topos.page = 1; loadTopoAdmin();
}));
$('logSearch').addEventListener('input', debounce(function () {
state.logs.q = this.value.trim(); state.logs.page = 1; loadLogs();
}));
$('topoOwner').addEventListener('change', function () {
state.topos.owner = this.value; state.topos.page = 1; loadTopoAdmin();
});
$('topoSelectAll').addEventListener('change', function () {
const checked = this.checked;
Array.prototype.forEach.call(document.querySelectorAll('#topoRows .topo-chk'), function (c) {
c.checked = checked;
});
});
$('btnExportSelectedTopos').addEventListener('click', function () { exportTopos(false); });
$('btnExportAllTopos').addEventListener('click', function () { exportTopos(true); });
$('btnDeleteSelectedTopos').addEventListener('click', deleteSelectedTopos);
$('btnSaveSettings').addEventListener('click', saveSettings);
$('btnPickLogo').addEventListener('click', function () { $('logoFile').click(); });
$('logoFile').addEventListener('change', onPickLogo);
$('btnClearLogo').addEventListener('click', function () { logoValue = ''; renderLogoPreview(''); });
$('userRows').addEventListener('click', onUserRowClick);
$('topoRows').addEventListener('click', onTopoRowClick);
$('btnNewUser').addEventListener('click', openUserDialog);
$('btnSaveUser').addEventListener('click', submitUserDialog);
$('btnCancelUser').addEventListener('click', function () { closeOverlay('userOverlay'); });
$('btnCloseUser').addEventListener('click', function () { closeOverlay('userOverlay'); });
$('uPass').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); submitUserDialog(); } });
$('btnDoReset').addEventListener('click', submitResetDialog);
$('btnCancelReset').addEventListener('click', function () { closeOverlay('resetOverlay'); });
$('btnCloseReset').addEventListener('click', function () { closeOverlay('resetOverlay'); });
$('resetShow').addEventListener('change', function () {
const t = this.checked ? 'text' : 'password';
['resetAdminPw', 'resetNewPw', 'resetNewPw2'].forEach(function (i) { $(i).type = t; });
});
['resetNewPw', 'resetNewPw2'].forEach(function (i) {
$(i).addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); submitResetDialog(); } });
});
$('confirmOk').addEventListener('click', _dlgConfirmOk);
$('confirmCancel').addEventListener('click', function () { _dlgFinish(false); });
$('confirmClose').addEventListener('click', function () { _dlgFinish(false); });
$('confirmField').addEventListener('keydown', function (e) {
if (e.key === 'Enter') { e.preventDefault(); _dlgConfirmOk(); }
});
}
/* ============================================================
启动:校验管理员身份
============================================================ */
function boot() {
bindEvents();
API.me().then(function (d) {
const u = d.user;
if (!u || u.role !== 'admin') {
toast('需要管理员权限,正在返回…', 'warn');
setTimeout(function () { window.location.href = '../index.php'; }, 900);
return;
}
state.currentUser = u;
$('navUser').textContent = u.username + ' · 管理员';
loadSettings();
switchView('overview');
}).catch(function () {
window.location.href = '../index.php';
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();