女朋友炸了:刚打开的网页怎么又没了?我反手甩出一键恢复按钮!

女朋友经常手滑关掉标签页这事儿头大了?我也一样。于是干脆写了个小而美的 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 = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;',
  };
  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);

安装方式(开发者模式)

  1. 打开 Chrome,访问 chrome://extensions/

  2. 右上角开启"开发者模式"

  3. 点击"加载已解压的扩展程序"

选择刚刚创建的recently-closed-tabs文件夹进行加载

  1. 在工具栏固定扩展图标,点击即可使用 如下图:

至此,我们的一键恢复插件就搞完了

使用说明

  • 打开扩展弹窗,即可看到最近关闭的标签页列表
  • 点击某一项恢复该标签页
  • 点击右上角"全部恢复"按钮,一次性恢复所有列表内的标签页
  • 若列表为空,会显示"没有最近关闭的标签页"

常见问题

  • 看不到任何记录?

    • 需要近期确实关闭过标签页;浏览器重启后记录可能被系统回收
  • 想调整条目显示数量?

    • 修改 popup.jsCONFIG.MAX_TABS,最大25

隐私与权限声明

  • 本扩展不采集任何用户数据
  • 数据来自浏览器内置 chrome.sessions API,仅在本地运行
  • 权限精简:sessionstabsfavicon

如果觉得对您有帮助,欢迎点赞 👍 收藏 ⭐ 关注 🔔 支持一下!

相关推荐
Renounce4 小时前
【Android】让 Android 界面 “动” 起来:动画知识点大起底
前端
Asort4 小时前
JavaScript设计模式(十四)——命令模式:解耦请求发送者与接收者
前端·javascript·设计模式
小茴香3534 小时前
Vue 脚手架(Vue CLI)
前端·javascript·vue.js
午安~婉4 小时前
ESLint
前端·eslint·检查
“抚琴”的人4 小时前
C#中获取程序执行时间
服务器·前端·c#
掘金一周4 小时前
Flex 布局下文字省略不生效?原因其实很简单| 掘金一周 10.16
前端
你的电影很有趣4 小时前
lesson72:Node.js 安全实战:Crypto-Js 4.2.0 与 Express 加密体系构建指南
javascript·安全·node.js
Stringzhua4 小时前
Vue的Axios介绍【9】
前端·javascript·vue.js
渣哥4 小时前
从 READ_UNCOMMITTED 到 SERIALIZABLE:Spring 事务隔离级别全解析
javascript·后端·面试