feat: add local TOTP user tracking

This commit is contained in:
王源
2026-10-20 16:39:15 +08:00
parent 903b058340
commit b735afbbfa
11 changed files with 657 additions and 145 deletions
+46 -116
View File
@@ -1,23 +1,43 @@
(() => {
const auth = window.TrafficAuth;
const current = auth.requireUser();
if (!current) return;
const templates = window.LINK_TEMPLATES;
const historyStore = window.LinkHistoryStore;
if (!templates?.length) return;
const { convertTrackingLink } = window.TrackingLinkConverter;
const HISTORY_KEY = 'traffic_link_history_v1';
const MAX_HISTORY = 50;
const $ = (id) => document.getElementById(id);
const platformSelect = $('platformSelect');
const macroBody = $('macroTableBody');
const output = $('urlOutput');
let activeResult = null;
let history = loadHistory();
let inputTimer;
$('currentUser').innerHTML = `<b>${escapeHtml(current.username)}</b><small>${current.role === 'admin' ? '管理员' : '普通用户'}</small>`;
$('templateCount').textContent = `${templates.length} 个平台模板`;
templates.forEach((template, index) => platformSelect.add(new Option(template.name, index)));
const activeTemplate = () => templates[Number(platformSelect.value || 0)];
function ensureSession() {
const active = auth.currentUser();
if (active?.username === current.username) return true;
location.replace('login.html?next=link-generator.html');
return false;
}
auth.watchSession(() => location.replace('login.html?next=link-generator.html'));
function scopedHistoryCount() {
const records = historyStore.list();
return current.role === 'admin' ? records.length : records.filter((item) => item.username === current.username).length;
}
function updateHistoryCount() {
$('historyCountBadge').textContent = scopedHistoryCount();
}
function renderPlatform() {
const template = activeTemplate();
const review = template.status.includes('待');
@@ -31,18 +51,12 @@
const allMacros = activeTemplate().macros;
const rows = allMacros.filter(([macro, desc]) => `${macro} ${desc}`.toLowerCase().includes(query));
$('macroCount').textContent = `${allMacros.length} 个可用宏 · 点击展开`;
macroBody.innerHTML = rows.length
? rows.map(([macro, desc]) => `<tr><td><code>${escapeHtml(macro)}</code></td><td>${escapeHtml(desc)}</td></tr>`).join('')
: '<tr><td colspan="2" class="table-empty">没有匹配的媒体宏</td></tr>';
macroBody.innerHTML = rows.length ? rows.map(([macro, desc]) => `<tr><td><code>${escapeHtml(macro)}</code></td><td>${escapeHtml(desc)}</td></tr>`).join('') : '<tr><td colspan="2" class="table-empty">没有匹配的媒体宏</td></tr>';
}
function renderChanges(changes) {
const rows = changes.length ? changes : [{ key:'—', from:'原链接参数无需变化', to:'—' }];
$('mappingTableBody').innerHTML = rows.map((change) => `<tr>
<td><code>${escapeHtml(change.key)}</code></td>
<td><span class="old-param">${escapeHtml(change.from || '(空值)')}</span></td>
<td>${change.to ? `<code>${escapeHtml(change.to)}</code>` : '<span class="empty-param">留空</span>'}</td>
</tr>`).join('');
$('mappingTableBody').innerHTML = rows.map((change) => `<tr><td><code>${escapeHtml(change.key)}</code></td><td><span class="old-param">${escapeHtml(change.from || '(空值)')}</span></td><td>${change.to ? `<code>${escapeHtml(change.to)}</code>` : '<span class="empty-param">留空</span>'}</td></tr>`).join('');
}
function showEmpty() {
@@ -72,7 +86,6 @@
if (showRequiredMessage) $('generatorMessage').textContent = '请先粘贴一条完整的 Tracking Link';
return null;
}
try {
const result = convertTrackingLink(original, activeTemplate());
activeResult = result;
@@ -95,11 +108,19 @@
}
}
function extractPkg(url) {
try { return new URL(url).searchParams.get('pkg') || '—'; }
catch (_) { return '—'; }
}
function convertAndSave() {
if (!ensureSession()) return;
const result = convertFromInput(true);
if (!result) return;
saveHistory(result);
$('generatorMessage').textContent = '转换完成,已加入平台链接总结';
const saved = historyStore.add({ username:current.username, userRole:current.role, platform:activeTemplate().name, pkg:extractPkg(result.url), type:result.type, url:result.url });
if (!saved.ok) return showToast('转换成功,但浏览器无法保存记录');
updateHistoryCount();
$('generatorMessage').textContent = '转换完成,已记录操作用户';
showToast('转换完成并已保存记录');
}
@@ -108,130 +129,39 @@
convertFromInput(false);
}
function loadHistory() {
try {
const stored = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');
return Array.isArray(stored) ? stored.filter((item) => item?.id && item?.url).slice(0, MAX_HISTORY) : [];
} catch (_) { return []; }
}
function persistHistory() {
try {
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
return true;
} catch (_) {
showToast('浏览器无法保存记录,请检查存储权限');
return false;
}
}
function extractPkg(url) {
try { return new URL(url).searchParams.get('pkg') || '—'; }
catch (_) { return '—'; }
}
function saveHistory(result) {
const platform = activeTemplate().name;
const pkg = extractPkg(result.url);
history = history.filter((item) => item.url !== result.url);
history.unshift({
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
platform,
pkg,
type: result.type,
createdAt: new Date().toISOString(),
url: result.url
});
history = history.slice(0, MAX_HISTORY);
persistHistory();
renderHistory();
}
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 renderHistory() {
$('historyCount').textContent = history.length;
$('clearHistory').disabled = history.length === 0;
$('historyEmpty').classList.toggle('hidden', history.length > 0);
$('historyTableWrap').classList.toggle('hidden', history.length === 0);
$('historyTableBody').innerHTML = history.map((item) => `<tr>
<td><time datetime="${escapeHtml(item.createdAt)}">${escapeHtml(formatTime(item.createdAt))}</time></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)}" aria-label="删除这条记录">删除</button></td>
</tr>`).join('');
}
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();
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();
}
}
async function copyUrl() {
if (!activeResult) return;
if (!activeResult || !ensureSession()) return;
await copyText(activeResult.url);
$('copyStatus').textContent = '已复制';
showToast('转换后的平台链接已复制');
$('copyStatus').textContent = '已复制'; showToast('转换后的平台链接已复制');
}
function showToast(message) {
const toast = $('toast');
toast.textContent = message;
toast.classList.add('show');
clearTimeout(showToast.timer);
showToast.timer = setTimeout(() => toast.classList.remove('show'), 1800);
const toast = $('toast'); toast.textContent = message; toast.classList.add('show');
clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 1800);
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (char) => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' })[char]);
}
$('logoutBtn').addEventListener('click', () => { auth.logout(); location.href = 'login.html'; });
platformSelect.addEventListener('change', renderPlatform);
$('macroSearch').addEventListener('input', renderMacros);
$('convertLink').addEventListener('click', convertAndSave);
$('sampleLink').addEventListener('click', loadSample);
$('copyLink').addEventListener('click', copyUrl);
$('clearConverter').addEventListener('click', () => { $('originalLink').value = ''; showEmpty(); $('originalLink').focus(); });
$('clearHistory').addEventListener('click', () => {
if (!history.length || !window.confirm('确定清空全部平台链接记录吗?')) return;
history = [];
persistHistory();
renderHistory();
});
$('historyTableBody').addEventListener('click', async (event) => {
const button = event.target.closest('button[data-action]');
if (!button) return;
const item = history.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') {
history = history.filter((record) => record.id !== item.id);
persistHistory();
renderHistory();
}
});
$('originalLink').addEventListener('input', () => {
clearTimeout(inputTimer);
inputTimer = setTimeout(() => convertFromInput(false), 220);
});
$('originalLink').addEventListener('input', () => { clearTimeout(inputTimer); inputTimer = setTimeout(() => convertFromInput(false), 220); });
window.addEventListener('storage', (event) => { if (event.key === historyStore.STORAGE_KEY) updateHistoryCount(); });
renderPlatform();
showEmpty();
renderHistory();
renderPlatform(); showEmpty(); updateHistoryCount();
})();