feat: add local TOTP user tracking
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
(() => {
|
||||
const auth = window.TrafficAuth;
|
||||
const store = window.LinkHistoryStore;
|
||||
const current = auth.requireUser();
|
||||
if (!current) return;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
let records = [];
|
||||
|
||||
function ensureSession() {
|
||||
const active = auth.currentUser();
|
||||
if (active?.username === current.username) return true;
|
||||
location.replace('login.html?next=link-history.html');
|
||||
return false;
|
||||
}
|
||||
|
||||
auth.watchSession(() => location.replace('login.html?next=link-history.html'));
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/[&<>"']/g, (char) => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' })[char]);
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '—';
|
||||
return new Intl.DateTimeFormat('zh-CN', { year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false }).format(date);
|
||||
}
|
||||
|
||||
function scopedRecords() {
|
||||
const all = store.list();
|
||||
return current.role === 'admin' ? all : all.filter((item) => item.username === current.username);
|
||||
}
|
||||
|
||||
function filteredRecords() {
|
||||
const query = $('historySearch').value.trim().toLowerCase();
|
||||
return query ? records.filter((item) => `${item.username} ${item.platform} ${item.pkg} ${item.url}`.toLowerCase().includes(query)) : records;
|
||||
}
|
||||
|
||||
function render() {
|
||||
records = scopedRecords();
|
||||
const filtered = filteredRecords();
|
||||
$('historyTotal').textContent = records.length;
|
||||
$('filterCount').textContent = `显示 ${filtered.length} 条`;
|
||||
$('clearHistory').disabled = records.length === 0;
|
||||
$('clearHistory').textContent = current.role === 'admin' ? '清空本机全部记录' : '清空我的记录';
|
||||
$('historyEmpty').classList.toggle('hidden', records.length > 0);
|
||||
$('historyTableWrap').classList.toggle('hidden', records.length === 0);
|
||||
$('historyTableBody').innerHTML = filtered.length ? filtered.map((item) => `<tr>
|
||||
<td><time datetime="${escapeHtml(item.createdAt)}">${escapeHtml(formatTime(item.createdAt))}</time></td>
|
||||
<td><span class="record-user">${escapeHtml(item.username)}</span></td>
|
||||
<td><span class="history-platform">${escapeHtml(item.platform)}</span></td>
|
||||
<td><code class="history-pkg">${escapeHtml(item.pkg)}</code></td>
|
||||
<td><div class="history-link"><span title="${escapeHtml(item.url)}">${escapeHtml(item.url)}</span><button type="button" data-action="copy" data-id="${escapeHtml(item.id)}">复制</button></div></td>
|
||||
<td><button class="delete-record" type="button" data-action="delete" data-id="${escapeHtml(item.id)}">删除</button></td>
|
||||
</tr>`).join('') : '<tr><td colspan="6" class="table-empty">没有匹配的链接记录</td></tr>';
|
||||
}
|
||||
|
||||
async function copyText(text) {
|
||||
try { await navigator.clipboard.writeText(text); }
|
||||
catch (_) {
|
||||
const area = document.createElement('textarea'); area.value = text;
|
||||
area.style.position = 'fixed'; area.style.opacity = '0';
|
||||
document.body.appendChild(area); area.select(); document.execCommand('copy'); area.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
const toast = $('toast'); toast.textContent = message; toast.classList.add('show');
|
||||
clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 1800);
|
||||
}
|
||||
|
||||
$('currentUser').innerHTML = `<b>${escapeHtml(current.username)}</b><small>${current.role === 'admin' ? '管理员' : '普通用户'}</small>`;
|
||||
$('historyScope').textContent = current.role === 'admin' ? '管理员可查看当前浏览器内全部用户的记录。' : '普通用户仅可查看当前浏览器内自己的记录。';
|
||||
$('logoutBtn').addEventListener('click', () => { auth.logout(); location.href = 'login.html'; });
|
||||
$('historySearch').addEventListener('input', render);
|
||||
$('clearHistory').addEventListener('click', () => {
|
||||
if (!ensureSession()) return;
|
||||
if (!records.length || !window.confirm(current.role === 'admin' ? '确定清空当前浏览器的全部链接记录吗?' : '确定清空你的全部链接记录吗?')) return;
|
||||
const result = current.role === 'admin' ? store.clear() : store.replace(store.list().filter((item) => item.username !== current.username));
|
||||
if (!result.ok) return showToast('清空失败,请检查浏览器存储权限');
|
||||
$('historySearch').value = ''; render(); showToast('链接记录已清空');
|
||||
});
|
||||
$('historyTableBody').addEventListener('click', async (event) => {
|
||||
if (!ensureSession()) return;
|
||||
const button = event.target.closest('button[data-action]');
|
||||
if (!button) return;
|
||||
const item = records.find((record) => record.id === button.dataset.id);
|
||||
if (!item) return;
|
||||
if (button.dataset.action === 'copy') { await copyText(item.url); showToast('链接已复制'); }
|
||||
else if (button.dataset.action === 'delete') { if (store.remove(item.id).ok) render(); }
|
||||
});
|
||||
window.addEventListener('storage', (event) => { if (event.key === store.STORAGE_KEY) render(); });
|
||||
render();
|
||||
})();
|
||||
Reference in New Issue
Block a user