参考了站内几位大佬的脚本,把入口由悬浮窗改为顶栏,风格也更贴合
优雅++,舒适++ ![]()
功能如下:
- 自定义保留时长
- 自定义保留数目
- 自定义时间排序
- PC-移动端支持,深-浅色支持
- 支持图标入口右上角角标开关
特殊说明:
100%字研,Codex立大功 ![]()
欢迎体验与反馈或者二创
点击获取脚本
参考了站内几位大佬的脚本,把入口由悬浮窗改为顶栏,风格也更贴合
优雅++,舒适++ ![]()
功能如下:
特殊说明:
100%字研,Codex立大功 ![]()
欢迎体验与反馈或者二创
点击获取脚本
佬牛啊!
cool
得感谢GPT-5.2,主播学嵌入式的,根本不会js ![]()
挺好的 支持一下
好看的呢
提个功能上的建议,点击框选的按钮,能够跳转 https://linux.do/u/[id]/activity/read 即官方的历史记录
ok,确实可以加一个
另外这个角标能不能给个开关,强迫症看着很难受
毕竟好像不怎么需要关注历史记录有多少条

也可以
其实之前一版是蓝色的,我嫌太显眼才改成这样的 ![]()
感谢大佬!
全部都实现了,你看看有没有bug,我这边测试应该是可以用的 ![]()
可能有些bug
感谢分享
上面的无法编辑了,再更新一下,修复了一些显示问题
// ==UserScript==
// @name Linux.do 浏览历史记录
// @namespace http://tampermonkey.net/
// @version 2.42
// @description 记录并显示 Linux.do 的浏览历史(修复标题过长换行问题)
// @author GPT, Gemini
// @match https://linux.do/*
// @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%2212%22%20fill%3D%22%23f7f3e8%22/%3E%3Cpath%20fill%3D%22%23222%22%20d%3D%22M13%203a9%209%200%200%200-9%209H1l3.89%203.89.07.14L9%2012H6c0-3.87%203.13-7%207-7s7%203.13%207%207-3.13%207-7%207c-1.93%200-3.68-.79-4.94-2.06l-1.42%201.42A8.954%208.954%200%200%200%2013%2021a9%209%200%200%200%200-18zm-1%205v5l4.28%202.54.72-1.21-3.5-2.08V8H12z%22/%3E%3C/svg%3E
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// ===== 常量配置 =====
const KEYS = {
STORAGE: 'linuxdo_browse_history',
VIEW_MODE: 'linuxdo_browse_history_view_mode',
RETENTION: 'linuxdo_browse_history_retention',
MAX_SIZE: 'linuxdo_browse_history_max_size',
SHOW_BADGE: 'linuxdo_browse_history_show_badge'
};
const RECENT_LIMIT = 50;
const SEARCH_RESULT_LIMIT = 200;
const DEFAULT_MAX_SIZE = 2000;
const MAX_SIZE_OPTIONS = [100, 200, 500, 1000, 2000, 5000, 10000];
// 图标常量
const ICONS = {
GEAR: `<svg class="fa d-icon d-icon-cog svg-icon svg-string" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.04-.7-1.64-.94l-.37-2.85A.507.507 0 0 0 13.94 2h-3.88c-.25 0-.47.18-.5.43l-.37 2.85c-.6.24-1.15.56-1.64.94l-2.39-.96a.495.495 0 0 0-.59.22L2.65 8.8c-.12.21-.08.48.12.61l2.03 1.58c-.05.3-.07.62-.07.94 0 .31.02.63.06.94L2.72 14.52c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.04.7 1.64.94l.37 2.85c.03.25.25.43.5.43h3.88c.25 0 .47-.18.5-.43l.37-2.85c.6-.24 1.15-.56 1.64-.94l2.39.96c.22.09.47 0 .59-.22l1.92-3.32c.12-.22.08-.48-.12-.61l-2.01-1.58zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5S10.07 8.5 12 8.5s3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></svg>`,
ARROW: `<svg class="fa d-icon d-icon-arrow-left svg-icon svg-string" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>`,
TRASH: `<svg class="fa d-icon d-icon-trash-can svg-icon svg-string" viewBox="0 0 24 24"><path d="M9 3h6l1 2h5v2H3V5h5l1-2zm-2 6h2v10h2V9h2v10h2V9h2v10h2V9h2v10a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V9z"/></svg>`,
SEARCH: `<svg class="fa d-icon d-icon-search svg-icon svg-string" viewBox="0 0 24 24"><path d="M21.71 20.29l-3.38-3.38A7.92 7.92 0 0 0 19 11a8 8 0 1 0-8 8 7.92 7.92 0 0 0 5.91-2.67l3.38 3.38a1 1 0 0 0 1.42-1.42zM11 17a6 6 0 1 1 6-6 6 6 0 0 1-6 6z"/></svg>`,
CLOCK: `<svg class="fa d-icon d-icon-clock svg-icon svg-string" viewBox="0 0 24 24"><path d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>`
};
// ===== 状态变量 =====
let currentSearchQuery = '';
let historyCache = null;
let historyCacheRaw = null;
let historyCacheDirty = true;
// ===== 工具函数 =====
const debounce = (func, wait) => {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
};
function safeJSONParse(str, fallback = []) {
try { return JSON.parse(str) || fallback; } catch (e) { return fallback; }
}
// ===== 数据层 =====
function getSetting(key, def, normalize) {
try {
const val = GM_getValue(key);
return normalize ? normalize(val) : (val ?? def);
} catch (e) { return def; }
}
function saveSetting(key, val, normalize) {
try {
const v = normalize ? normalize(val) : val;
GM_setValue(key, v);
if (key === KEYS.RETENTION || key === KEYS.MAX_SIZE) historyCacheDirty = true;
} catch (e) {}
}
const Settings = {
getViewMode: () => getSetting(KEYS.VIEW_MODE, 'recent', v => v === 'all' ? 'all' : 'recent'),
saveViewMode: v => saveSetting(KEYS.VIEW_MODE, v),
getRetention: () => getSetting(KEYS.RETENTION, 'permanent', v => ['7d','14d','1m','3m','6m','1y','permanent'].includes(v) ? v : 'permanent'),
saveRetention: v => saveSetting(KEYS.RETENTION, v),
getMaxSize: () => getSetting(KEYS.MAX_SIZE, DEFAULT_MAX_SIZE, v => {
const n = Number(v); return (n > 0) ? MAX_SIZE_OPTIONS.reduce((p,c) => Math.abs(c-n) < Math.abs(p-n)?c:p) : DEFAULT_MAX_SIZE;
}),
saveMaxSize: v => saveSetting(KEYS.MAX_SIZE, v),
getShowBadge: () => getSetting(KEYS.SHOW_BADGE, true, v => !(v === false || v === 'false' || v === '0')),
saveShowBadge: v => saveSetting(KEYS.SHOW_BADGE, v)
};
function getHistory() {
try {
const raw = GM_getValue(KEYS.STORAGE, '[]');
if (!historyCacheDirty && historyCache && raw === historyCacheRaw) return historyCache;
let list = safeJSONParse(raw);
if (!Array.isArray(list)) list = [];
const map = new Map();
for (const item of list) {
if (!item.topicId || !item.url) continue;
const existing = map.get(item.topicId);
if (existing) {
existing.lastVisitedAt = Math.max(existing.lastVisitedAt || 0, item.lastVisitedAt || item.timestamp || 0);
existing.firstVisitedAt = Math.min(existing.firstVisitedAt || Infinity, item.firstVisitedAt || item.timestamp || Infinity);
existing.visitCount = (existing.visitCount || 1) + (item.visitCount || 1);
existing.title = item.title || existing.title;
} else {
map.set(item.topicId, {
...item,
lastVisitedAt: item.lastVisitedAt || item.timestamp || 0,
firstVisitedAt: item.firstVisitedAt || item.timestamp || 0,
visitCount: item.visitCount || 1
});
}
}
list = Array.from(map.values());
const ret = Settings.getRetention();
if (ret !== 'permanent') {
const daysMap = { '7d':7, '14d':14, '1m':30, '3m':90, '6m':180, '1y':365 };
const limit = Date.now() - (daysMap[ret] * 86400000);
list = list.filter(i => i.lastVisitedAt >= limit);
}
list.sort((a, b) => b.lastVisitedAt - a.lastVisitedAt);
const maxSize = Settings.getMaxSize();
if (list.length > maxSize) list = list.slice(0, maxSize);
const newRaw = JSON.stringify(list);
if (newRaw !== raw) GM_setValue(KEYS.STORAGE, newRaw);
historyCache = list;
historyCacheRaw = newRaw;
historyCacheDirty = false;
return list;
} catch (e) { return []; }
}
function addToHistory() {
const url = location.href;
const match = url.match(/linux\.do\/t\/([^\/]+)\/(\d+)/);
if (!match) return;
const titleEl = document.querySelector('h1 a.fancy-title') || document.querySelector('.fancy-title') || document.querySelector('h1');
const title = titleEl?.textContent.trim();
if (!title) return;
const topicId = match[2];
const cleanUrl = `https://linux.do/t/${match[1]}/${match[2]}`;
const now = Date.now();
const history = getHistory();
const idx = history.findIndex(i => String(i.topicId) === topicId);
let item = idx >= 0 ? history.splice(idx, 1)[0] : { topicId, url: cleanUrl, firstVisitedAt: now, visitCount: 0 };
item.title = title;
item.lastVisitedAt = now;
item.visitCount = (item.visitCount || 0) + 1;
history.unshift(item);
const maxSize = Settings.getMaxSize();
if (history.length > maxSize) history.length = maxSize;
GM_setValue(KEYS.STORAGE, JSON.stringify(history));
historyCacheDirty = true;
updateHistoryPanel();
}
let saveTimer = null;
function scheduleSave() {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(addToHistory, 1500);
}
// ===== 导入导出 =====
function exportData() {
const data = GM_getValue(KEYS.STORAGE, '[]');
const blob = new Blob([data], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
a.href = url;
a.download = `linuxdo_history_${date}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function importData(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const imported = JSON.parse(e.target.result);
if (!Array.isArray(imported)) throw new Error('无效的格式');
const current = safeJSONParse(GM_getValue(KEYS.STORAGE, '[]'));
const merged = [...imported, ...current];
GM_setValue(KEYS.STORAGE, JSON.stringify(merged));
historyCacheDirty = true;
alert(`导入成功!共合并处理记录,面板将刷新。`);
updateHistoryPanel();
} catch (err) { alert('导入失败:文件格式不正确'); }
};
reader.readAsText(file);
}
// ===== UI 构建 =====
function injectStyles() {
const css = `
#ld-history-container { position: relative; }
#ld-history-btn { position: relative; }
#ld-history-btn svg { fill: currentColor; }
#ld-history-badge {
position: absolute; top: -3px; right: -3px; min-width: 18px; height: 18px;
padding: 0 4px; border-radius: 9px; background: var(--primary-low-mid, #cfcfcf);
color: var(--primary, #333); font-size: 11px; line-height: 18px;
text-align: center; display: none; opacity: 0.9; pointer-events: none;
}
#ld-history-badge.show { display: inline-block; }
#ld-history-panel {
position: absolute; top: calc(100% + 8px); right: 0;
width: min(360px, calc(100vw - 24px)); max-height: 0;
background: var(--secondary, #fff); border: 1px solid var(--primary-low, #e5e5e5);
border-radius: 4px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2);
z-index: 99999; overflow: hidden; opacity: 0; pointer-events: none;
transition: max-height 0.2s ease, opacity 0.2s ease; display: flex; flex-direction: column;
}
#ld-history-panel.show { max-height: min(560px, calc(100vh - 72px)); opacity: 1; pointer-events: auto; }
.ld-history-header {
padding: 8px 10px; background: var(--primary-low, #f6f6f6); color: var(--primary, #333);
font-weight: bold; font-size: 14px; display: flex; flex-shrink: 0;
justify-content: space-between; align-items: center; gap: 4px;
border-bottom: 1px solid var(--primary-low, #e5e5e5);
flex-wrap: nowrap; /* 强制不换行 */
}
.ld-history-header-title {
display: flex; align-items: baseline; gap: 4px; flex: 1;
overflow: hidden; min-width: 0; /* 允许子元素缩小 */
white-space: nowrap; /* 核心:禁止文字换行 */
}
.ld-history-header-title-text {
background:none; border:none; padding:0; font:inherit; color:inherit; cursor:pointer;
white-space: nowrap; flex-shrink: 0;
}
.ld-history-count {
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
font-weight: normal; font-size: 11px; opacity: 0.8;
}
.ld-history-header-actions { display: flex; gap: 4px; flex-shrink: 0; }
.ld-history-header-icons { display: flex; gap: 2px; flex-shrink: 0; }
.ld-icon-btn {
width: 28px; height: 28px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
color: var(--primary-high, #555); background: transparent; border: none; cursor: pointer; border-radius: 4px;
}
.ld-icon-btn:hover { background: var(--primary-low-mid, #e5e5e5); color: var(--primary, #222); }
/* 强制约束SVG尺寸,防止d-icon样式污染导致的布局崩坏 */
.ld-icon-btn svg { width: 14px !important; height: 14px !important; fill: currentColor; pointer-events: none; max-width: 14px; max-height: 14px; }
.ld-search-bar { display: none; flex-shrink: 0; padding: 8px 10px; background: var(--primary-low, #f6f6f6); border-bottom: 1px solid var(--primary-low, #e5e5e5); }
.ld-search-bar.show { display: block; }
.ld-search-input { width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid var(--primary-medium, #999); border-radius: 4px; font-size: 13px; }
.ld-history-list { flex: 1; overflow-y: auto; padding: 0; min-height: 0; }
.ld-history-settings { display: none; flex: 1; overflow-y: auto; padding: 10px 12px; min-height: 0; }
#ld-history-panel.view-settings .ld-history-settings { display: block; }
#ld-history-panel.view-settings .ld-history-list,
#ld-history-panel.view-settings .ld-history-header-actions,
#ld-history-panel.view-settings .ld-search-bar { display: none; }
.ld-settings-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.ld-settings-section-title { font-size: 13px; font-weight: 600; }
.ld-settings-value { font-size: 12px; font-weight: 600; }
.ld-retention-slider { width: 100%; margin-bottom: 6px; accent-color: var(--tertiary, #0088cc); }
.ld-settings-toggle-row { display: flex; align-items: center; gap: 4px; margin-top: 10px; cursor: pointer; }
.ld-settings-actions { display: flex; justify-content: flex-end; margin-top: 10px; gap: 8px; }
.ld-settings-divider { height: 1px; background: var(--primary-low, #e5e5e5); margin: 15px 0 10px; }
.ld-history-item { display: block; padding: 10px 12px; border-bottom: 1px solid var(--primary-low, #f0f0f0); text-decoration: none; color: inherit; cursor: pointer; }
.ld-history-item:hover { background: var(--primary-low, #f6f6f6); }
.ld-history-title { font-size: 14px; color: var(--primary, #333); line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.ld-history-time { font-size: 12px; color: var(--primary-medium, #777); margin-top: 4px; }
.ld-history-empty { padding: 32px; text-align: center; color: var(--primary-medium, #777); }
.ld-history-list::-webkit-scrollbar { width: 4px; }
.ld-history-list::-webkit-scrollbar-thumb { background: var(--primary-low-mid, #cfcfcf); border-radius: 6px; }
@media (max-width: 480px) {
#ld-history-panel { position: fixed; left: 8px; right: 8px; width: auto; top: var(--ld-history-panel-top, 56px); }
}
`;
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
}
function formatTime(ts) {
const diff = Date.now() - ts;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff/60000) + ' 分钟前';
if (diff < 86400000) return Math.floor(diff/3600000) + ' 小时前';
return Math.floor(diff/86400000) + ' 天前';
}
// 尝试使用sprite提升主题兼容性,但也提供fallback
function upgradeIcon(btn, idStr) {
if (!btn) return;
const svg = btn.querySelector('svg');
if (!svg) return;
const syms = document.querySelectorAll('symbol[id]');
const found = Array.from(syms).find(s => idStr.includes(s.id) || idStr.some(k => s.id.includes(k)));
if (found) {
svg.innerHTML = `<use href="#${found.id}"></use>`;
svg.classList.add('d-icon', `d-icon-${found.id}`);
svg.removeAttribute('viewBox'); // sprite通常不需要viewBox
}
}
function renderSettingsView(container, panel) {
const ret = Settings.getRetention();
const size = Settings.getMaxSize();
const badge = Settings.getShowBadge();
container.innerHTML = `
<div class="ld-settings-row"><span class="ld-settings-section-title">保留时间</span><span class="ld-settings-value" id="val-ret">${ret==='permanent'?'永久':ret}</span></div>
<input type="range" class="ld-retention-slider" id="rng-ret" min="0" max="6" value="${['7d','14d','1m','3m','6m','1y','permanent'].indexOf(ret)}">
<div class="ld-settings-row"><span class="ld-settings-section-title">最大条数</span><span class="ld-settings-value" id="val-size">${size}条</span></div>
<input type="range" class="ld-retention-slider" id="rng-size" min="0" max="${MAX_SIZE_OPTIONS.length-1}" value="${MAX_SIZE_OPTIONS.indexOf(size)}">
<label class="ld-settings-toggle-row"><span class="ld-settings-section-title">显示角标</span><input type="checkbox" id="chk-badge" ${badge?'checked':''}></label>
<div class="ld-settings-divider"></div>
<div class="ld-settings-row">
<span class="ld-settings-section-title">数据备份</span>
<div>
<button id="btn-export" class="btn btn-default btn-small">导出</button>
<button id="btn-import" class="btn btn-default btn-small">导入</button>
<input type="file" id="inp-file" style="display:none" accept=".json">
</div>
</div>
<div class="ld-settings-actions"><button id="btn-save" class="btn btn-primary btn-small">保存设置</button></div>
`;
const opts = ['7d','14d','1m','3m','6m','1y','permanent'];
const rngRet = container.querySelector('#rng-ret');
const rngSize = container.querySelector('#rng-size');
rngRet.oninput = () => container.querySelector('#val-ret').textContent = (opts[rngRet.value] === 'permanent' ? '永久' : opts[rngRet.value]);
rngSize.oninput = () => container.querySelector('#val-size').textContent = MAX_SIZE_OPTIONS[rngSize.value] + '条';
container.querySelector('#btn-export').onclick = (e) => { e.stopPropagation(); exportData(); };
const inpFile = container.querySelector('#inp-file');
container.querySelector('#btn-import').onclick = (e) => { e.stopPropagation(); inpFile.click(); };
inpFile.onchange = (e) => { if(e.target.files[0]) importData(e.target.files[0]); e.target.value = ''; };
container.querySelector('#btn-save').onclick = (e) => {
e.stopPropagation();
Settings.saveRetention(opts[rngRet.value]);
Settings.saveMaxSize(MAX_SIZE_OPTIONS[rngSize.value]);
Settings.saveShowBadge(container.querySelector('#chk-badge').checked);
panel.classList.remove('view-settings');
// 退出设置页时,恢复齿轮图标
const setBtn = panel.querySelector('#ld-settings-btn');
if (setBtn) {
setBtn.title = '设置';
setBtn.innerHTML = ICONS.GEAR;
}
const hdr = panel.querySelector('.ld-history-header-title-text');
if (hdr) hdr.textContent = '浏览历史';
updateHistoryPanel();
};
}
function updateHistoryPanel() {
const panel = document.getElementById('ld-history-panel');
if (!panel) return;
const listEl = panel.querySelector('.ld-history-list');
const viewMode = Settings.getViewMode();
panel.dataset.viewMode = viewMode;
const viewBtn = panel.querySelector('#ld-view-toggle-btn');
if (viewBtn) {
viewBtn.textContent = viewMode === 'all' ? '全部记录' : '最近50条';
viewBtn.classList.toggle('btn-primary', viewMode === 'all');
}
const all = getHistory();
let display = [];
let infoText = '';
if (currentSearchQuery) {
const lowerQ = currentSearchQuery.toLowerCase();
display = all.filter(i => (i.title + i.url).toLowerCase().includes(lowerQ));
infoText = `(搜: ${display.length})`;
if (display.length > SEARCH_RESULT_LIMIT) display = display.slice(0, SEARCH_RESULT_LIMIT);
} else {
display = (viewMode === 'all') ? all : all.slice(0, RECENT_LIMIT);
infoText = (viewMode !== 'all' && all.length > RECENT_LIMIT) ? `( ${display.length}/${all.length})` : `(${all.length})`;
}
display.sort((a,b) => b.lastVisitedAt - a.lastVisitedAt);
if (panel.classList.contains('sort-ascending')) display.reverse();
const badgeEl = document.getElementById('ld-history-badge');
if (badgeEl) {
badgeEl.textContent = all.length > 99 ? '99+' : all.length;
badgeEl.classList.toggle('show', Settings.getShowBadge() && all.length > 0);
}
const countEl = panel.querySelector('.ld-history-count');
if (countEl) countEl.textContent = infoText;
if (!display.length) {
listEl.innerHTML = `<div class="ld-history-empty">${currentSearchQuery ? '无搜索结果' : '暂无记录'}</div>`;
return;
}
const esc = t => { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; };
listEl.innerHTML = display.map(i => `
<a class="ld-history-item" href="${esc(i.url)}">
<div class="ld-history-title">${esc(i.title)}</div>
<div class="ld-history-time">${formatTime(i.lastVisitedAt)}</div>
</a>
`).join('');
}
function ensureUI() {
if (document.getElementById('ld-history-container')) return true;
const host = document.querySelector('.d-header-icons') || document.querySelector('.header-buttons');
if (!host) return false;
const container = document.createElement(host.tagName === 'UL' ? 'li' : 'span');
container.id = 'ld-history-container';
if (host.tagName === 'UL') container.className = 'custom-header-icon-link';
container.innerHTML = `
<button id="ld-history-btn" class="btn no-text icon btn-flat" title="浏览历史">
${ICONS.CLOCK}
<span id="ld-history-badge"></span>
</button>
<div id="ld-history-panel" role="dialog">
<div class="ld-history-header">
<span class="ld-history-header-title">
<button class="ld-history-header-title-text" id="ld-goto-read">浏览历史</button>
<span class="ld-history-count"></span>
</span>
<div class="ld-history-header-actions">
<button id="ld-view-toggle-btn" class="btn btn-default btn-small">最近50条</button>
<button id="ld-sort-btn" class="btn btn-default btn-small">时间↑</button>
</div>
<div class="ld-history-header-icons">
<button id="ld-search-toggle-btn" class="ld-icon-btn" title="搜索">${ICONS.SEARCH}</button>
<button id="ld-clear-btn" class="ld-icon-btn" title="清空">${ICONS.TRASH}</button>
<button id="ld-settings-btn" class="ld-icon-btn" title="设置">${ICONS.GEAR}</button>
</div>
</div>
<div id="ld-search-bar" class="ld-search-bar">
<input type="text" id="ld-search-input" class="ld-search-input" placeholder="搜索历史..." autocomplete="off">
</div>
<div class="ld-history-list"></div>
<div class="ld-history-settings"></div>
</div>
`;
host.appendChild(container);
const panel = container.querySelector('#ld-history-panel');
const btn = container.querySelector('#ld-history-btn');
const searchBar = container.querySelector('#ld-search-bar');
const searchInput = container.querySelector('#ld-search-input');
btn.onclick = (e) => {
e.preventDefault(); e.stopPropagation();
const show = panel.classList.toggle('show');
if (show) {
if (window.innerWidth <= 480) panel.style.setProperty('--ld-history-panel-top', (btn.getBoundingClientRect().bottom + 8) + 'px');
panel.classList.remove('view-settings');
// 恢复默认图标状态
container.querySelector('#ld-settings-btn').innerHTML = ICONS.GEAR;
container.querySelector('.ld-history-header-title-text').textContent = '浏览历史';
upgradeIcon(container.querySelector('#ld-clear-btn'), ['trash']);
upgradeIcon(container.querySelector('#ld-search-toggle-btn'), ['search']);
if (currentSearchQuery) { searchBar.classList.add('show'); searchInput.value = currentSearchQuery; }
updateHistoryPanel();
}
};
container.querySelector('#ld-search-toggle-btn').onclick = (e) => {
e.stopPropagation();
if (searchBar.classList.toggle('show')) searchInput.focus();
else { currentSearchQuery = ''; searchInput.value = ''; updateHistoryPanel(); }
};
searchBar.onclick = (e) => e.stopPropagation();
const performSearch = debounce((q) => { currentSearchQuery = q; updateHistoryPanel(); }, 300);
searchInput.oninput = (e) => performSearch(e.target.value.trim());
container.querySelector('#ld-view-toggle-btn').onclick = (e) => {
e.stopPropagation();
Settings.saveViewMode(Settings.getViewMode() === 'all' ? 'recent' : 'all');
updateHistoryPanel();
};
container.querySelector('#ld-sort-btn').onclick = (e) => {
e.stopPropagation();
const isAsc = panel.classList.toggle('sort-ascending');
e.target.textContent = isAsc ? '时间↓' : '时间↑';
updateHistoryPanel();
};
container.querySelector('#ld-clear-btn').onclick = (e) => {
e.stopPropagation();
if(confirm('确定清空历史?')) { GM_setValue(KEYS.STORAGE, '[]'); historyCacheDirty = true; updateHistoryPanel(); }
};
const setBtn = container.querySelector('#ld-settings-btn');
const headerTitle = container.querySelector('.ld-history-header-title-text');
// 修复后的切换逻辑:直接替换innerHTML,避免样式错乱
setBtn.onclick = (e) => {
e.stopPropagation();
const isSet = panel.classList.toggle('view-settings');
if (isSet) {
renderSettingsView(container.querySelector('.ld-history-settings'), panel);
headerTitle.textContent = '设置';
setBtn.title = '返回';
setBtn.innerHTML = ICONS.ARROW;
} else {
headerTitle.textContent = '浏览历史';
setBtn.title = '设置';
setBtn.innerHTML = ICONS.GEAR;
updateHistoryPanel();
}
};
container.querySelector('#ld-goto-read').onclick = (e) => {
e.stopPropagation();
if(panel.classList.contains('view-settings')) return;
const meta = document.querySelector('meta[name="current_username"], meta[name="discourse_username"]');
const u = meta?.content?.trim() || window.Discourse?.User?.current?.username;
if (u) { window.open(`/u/${u}/activity/read`, '_blank'); panel.classList.remove('show'); }
};
container.querySelector('.ld-history-list').onclick = (e) => {
const item = e.target.closest('a.ld-history-item');
if (item && !e.ctrlKey && !e.metaKey && e.button !== 1) {
e.preventDefault();
panel.classList.remove('show');
window.location.href = item.getAttribute('href');
}
};
document.addEventListener('click', (e) => { if(!container.contains(e.target)) panel.classList.remove('show'); });
return true;
}
function init() {
injectStyles();
let retry = 0;
const tryCreate = () => {
if(ensureUI()) {
updateHistoryPanel();
scheduleSave();
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) { lastUrl = location.href; scheduleSave(); setTimeout(ensureUI, 0); }
}).observe(document.body, {childList:true, subtree:true});
const ps = history.pushState; history.pushState = function() { ps.apply(this, arguments); scheduleSave(); };
const rs = history.replaceState; history.replaceState = function() { rs.apply(this, arguments); scheduleSave(); };
} else if(retry++ < 30) setTimeout(tryCreate, 500);
};
tryCreate();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();