女朋友经常手滑关掉标签页这事儿头大了?我也一样。于是干脆写了个小而美的 Chrome 扩展:点一下就能看到"最近关闭的标签页",想恢复单个点一下,想全恢复一键搞定。还有网站图标、关闭时间、顺滑的小动画,装上就能用。代码已经开源,想改界面随便改。
先看效果: 打开弹窗 → 点击恢复单个 → 全部恢复

功能亮点
- 自动列出最近关闭的标签页(最多 25 个)
- 支持单个恢复、全部恢复
- 显示网站图标、标题、URL、关闭时间
- 平滑动效与简洁 UI
- 零后台进程,权限最小化(仅用 sessions、tabs、favicon)
目录结构
recently-closed-tabs
├─ manifest.json
├─ popup.html
├─ styles.css
└─ popup.js
完整代码
manifest.json
json
{
"manifest_version": 3,
"name": "最近关闭标签页管理器",
"version": "1.0",
"description": "查看和恢复最近关闭的标签页",
"permissions": ["sessions", "tabs", "favicon"],
"action": {
"default_popup": "popup.html",
"default_title": "最近关闭的标签页"
}
}
styles.css
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
html,
body {
width: 440px;
height: 600px;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display',
'Segoe UI', Roboto, sans-serif;
background: #ffffff;
color: #000000;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
flex-shrink: 0;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
padding: 20px 20px 16px;
border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.5px;
color: #000000;
}
.tab-count {
font-size: 13px;
color: #8e8e93;
font-weight: 400;
margin-top: 2px;
}
.restore-all {
background: #007aff;
color: white;
border: none;
padding: 9px 18px;
border-radius: 18px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
}
.restore-all:hover {
background: #0051d5;
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
}
.restore-all:active {
transform: scale(0.98);
}
.tab-list {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
background: #ffffff;
padding: 12px 16px;
}
.tab-list::-webkit-scrollbar {
width: 6px;
}
.tab-list::-webkit-scrollbar-track {
background: transparent;
}
.tab-list::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 3px;
}
.tab-list::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25);
}
.tab-item {
display: flex;
align-items: center;
padding: 12px 14px;
margin-bottom: 6px;
background: #f9f9f9;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
animation: fadeIn 0.3s ease-out backwards;
}
.tab-item:nth-child(1) {
animation-delay: 0.05s;
}
.tab-item:nth-child(2) {
animation-delay: 0.1s;
}
.tab-item:nth-child(3) {
animation-delay: 0.15s;
}
.tab-item:nth-child(4) {
animation-delay: 0.2s;
}
.tab-item:nth-child(5) {
animation-delay: 0.25s;
}
.tab-item:hover {
background: #f0f0f0;
transform: scale(1.005);
}
.tab-item:active {
transform: scale(0.995);
background: #e8e8e8;
}
.favicon {
width: 24px;
height: 24px;
margin-right: 12px;
border-radius: 6px;
flex-shrink: 0;
background: white;
padding: 2px;
}
.tab-info {
flex: 1;
overflow: hidden;
min-width: 0;
}
.tab-title {
font-size: 14px;
font-weight: 500;
margin: 0 0 3px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #000000;
letter-spacing: -0.2px;
line-height: 1.3;
}
.tab-url {
font-size: 12px;
color: #8e8e93;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 400;
}
.closed-time {
font-size: 12px;
color: #007aff;
margin-left: 12px;
padding: 5px 10px;
background: rgba(0, 122, 255, 0.1);
border-radius: 8px;
white-space: nowrap;
font-weight: 500;
}
.empty-message {
text-align: center;
color: #8e8e93;
padding: 80px 20px;
font-size: 15px;
font-weight: 400;
animation: fadeIn 0.5s ease-out;
}
.empty-message::before {
content: '📭';
display: block;
font-size: 64px;
margin-bottom: 16px;
animation: float 3s ease-in-out infinite;
}
.empty-message::after {
content: '没有最近关闭的标签页';
display: block;
margin-top: 8px;
font-size: 14px;
color: #c7c7cc;
}
.loading {
text-align: center;
padding: 60px 20px;
}
.loading::before {
content: '';
display: inline-block;
width: 40px;
height: 40px;
border: 3px solid rgba(0, 122, 255, 0.2);
border-top-color: #007aff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
popup.html
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="container">
<div class="header">
<div class="header-content">
<div>
<h1>最近关闭</h1>
<div class="tab-count" id="tabCount">0 个标签页</div>
</div>
<button class="restore-all" id="restoreAllBtn">全部恢复</button>
</div>
</div>
<div class="tab-list" id="tabList">
<div class="loading"></div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
popup.js
js
// DOM元素
const elements = {
tabList: document.getElementById('tabList'),
restoreAllBtn: document.getElementById('restoreAllBtn'),
tabCount: document.getElementById('tabCount'),
};
// 常量
const CONFIG = {
MAX_TABS: 25,
MESSAGES: {
EMPTY: '<div class="empty-message">没有找到最近关闭的标签页</div>',
RESTORED: '<div class="empty-message">所有标签页已恢复</div>',
},
};
/**
* 初始化应用
*/
function init() {
loadRecentlyClosedTabs();
elements.restoreAllBtn.addEventListener('click', restoreAllTabs);
}
/**
* 加载并渲染最近关闭的标签页
*/
function loadRecentlyClosedTabs() {
chrome.sessions.getRecentlyClosed(
{ maxResults: CONFIG.MAX_TABS },
(sessions) => {
const tabs = sessions
.filter((s) => s.tab)
.map((s) => ({
id: s.tab.sessionId,
title: s.tab.title || '无标题',
url: s.tab.url,
closedTime: s.lastModified * 1000,
}));
updateTabCount(tabs.length);
renderTabList(tabs);
}
);
}
/**
* 更新标签计数
*/
function updateTabCount(count) {
if (elements.tabCount) {
elements.tabCount.textContent = `${count} 个标签页`;
}
}
/**
* 渲染标签页列表
*/
function renderTabList(tabs) {
if (tabs.length === 0) {
elements.tabList.innerHTML = CONFIG.MESSAGES.EMPTY;
return;
}
const fragment = document.createDocumentFragment();
tabs.forEach((tab) => fragment.appendChild(createTabElement(tab)));
elements.tabList.innerHTML = '';
elements.tabList.appendChild(fragment);
}
/**
* 创建标签页元素
*/
function createTabElement(tab) {
const div = document.createElement('div');
div.className = 'tab-item';
div.dataset.sessionId = tab.id;
div.innerHTML = `
<img class="favicon" src="${getFaviconURL(tab.url)}" alt="">
<div class="tab-info">
<h3 class="tab-title">${escapeHTML(tab.title)}</h3>
<p class="tab-url">${escapeHTML(tab.url)}</p>
</div>
<div class="closed-time">${formatTime(tab.closedTime)}</div>
`;
div.addEventListener('click', () => restoreTab(tab.id, div));
return div;
}
/**
* 恢复单个标签页
*/
function restoreTab(sessionId, element) {
// 添加加载状态
element.style.opacity = '0.5';
element.style.pointerEvents = 'none';
chrome.sessions.restore(sessionId, (restored) => {
if (chrome.runtime.lastError) {
console.error('恢复失败:', chrome.runtime.lastError);
// 恢复状态
element.style.opacity = '1';
element.style.pointerEvents = 'auto';
return;
}
loadRecentlyClosedTabs();
});
}
/**
* 恢复所有标签页
*/
function restoreAllTabs() {
chrome.sessions.getRecentlyClosed(
{ maxResults: CONFIG.MAX_TABS },
(sessions) => {
const tabs = sessions.filter((s) => s.tab);
if (tabs.length === 0) return;
elements.restoreAllBtn.disabled = true;
Promise.all(
tabs.map(
(s) =>
new Promise((resolve) =>
chrome.sessions.restore(s.tab.sessionId, resolve)
)
)
).then(() => {
elements.tabList.innerHTML = CONFIG.MESSAGES.RESTORED;
elements.restoreAllBtn.disabled = false;
});
}
);
}
/**
* 转义HTML
*/
function escapeHTML(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (m) => map[m]);
}
/**
* 获取网站图标URL
*/
function getFaviconURL(url) {
const faviconUrl = new URL(chrome.runtime.getURL('/_favicon/'));
faviconUrl.searchParams.set('pageUrl', url);
faviconUrl.searchParams.set('size', '16');
return faviconUrl.toString();
}
/**
* 格式化时间
*/
function formatTime(timestamp) {
const diff = Date.now() - timestamp;
const units = [
[86400000, (d) => `${d}天前`],
[3600000, (h) => `${h}小时前`],
[60000, (m) => `${m}分钟前`],
];
for (const [unit, format] of units) {
const value = Math.floor(diff / unit);
if (value > 0) return format(value);
}
return '刚刚';
}
// 启动应用
document.addEventListener('DOMContentLoaded', init);
安装方式(开发者模式)

-
打开 Chrome,访问 chrome://extensions/
-
右上角开启"开发者模式"
-
点击"加载已解压的扩展程序"
选择刚刚创建的recently-closed-tabs文件夹进行加载
- 在工具栏固定扩展图标,点击即可使用 如下图:

至此,我们的一键恢复插件就搞完了
使用说明
- 打开扩展弹窗,即可看到最近关闭的标签页列表
- 点击某一项恢复该标签页
- 点击右上角"全部恢复"按钮,一次性恢复所有列表内的标签页
- 若列表为空,会显示"没有最近关闭的标签页"
常见问题
-
看不到任何记录?
- 需要近期确实关闭过标签页;浏览器重启后记录可能被系统回收
-
想调整条目显示数量?
- 修改
popup.js
中CONFIG.MAX_TABS
,最大25
- 修改
隐私与权限声明
- 本扩展不采集任何用户数据
- 数据来自浏览器内置
chrome.sessions
API,仅在本地运行 - 权限精简:
sessions
、tabs
、favicon
如果觉得对您有帮助,欢迎点赞 👍 收藏 ⭐ 关注 🔔 支持一下!