F12 控制台日志移植到页面案例

场景:

捕获 console 的所有输出并实时显示在页面上,这在移动端调试或无法打开 DevTools 的环境中特别有用。

bash 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面内 Console 日志查看器</title>
<style>
  :root {
    --bg: #0d1117;
    --panel: #161b22;
    --border: #30363d;
    --text: #c9d1d9;
    --log: #79c0ff;
    --info: #58a6ff;
    --warn: #d29922;
    --error: #f85149;
    --debug: #8b949e;
    --accent: #3fb950;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
    background: var(--bg);
    color: var(--text);
    display: flex;
    flex-direction: column;
    height: 100vh;
  }

  /* 顶部工具区:模拟触发日志的按钮 */
  .toolbar {
    padding: 12px 16px;
    background: var(--panel);
    border-bottom: 1px solid var(--border);
  }
  .toolbar h1 {
    font-size: 15px;
    margin: 0 0 10px;
    color: #fff;
    font-weight: 600;
  }
  .btn-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  button {
    border: 1px solid var(--border);
    background: #21262d;
    color: var(--text);
    padding: 6px 12px;
    border-radius: 6px;
    font-size: 13px;
    cursor: pointer;
    transition: background .15s;
  }
  button:hover { background: #30363d; }
  button.clear { border-color: var(--error); color: var(--error); margin-left: auto; }
  button.clear:hover { background: rgba(248,81,73,.1); }

  /* 控制台面板 */
  .console-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .console-header {
    padding: 8px 16px;
    background: var(--panel);
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 12px;
    color: var(--debug);
  }
  .console-header .dot {
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--accent);
    display: inline-block;
  }
  .filter-tabs {
    display: flex;
    gap: 4px;
    margin-left: auto;
  }
  .filter-tabs button {
    padding: 3px 10px;
    font-size: 11px;
  }
  .filter-tabs button.active {
    background: var(--accent);
    color: #000;
    border-color: var(--accent);
  }

  .console-body {
    flex: 1;
    overflow-y: auto;
    padding: 4px 0;
    font-family: "SFMono-Regular", Consolas, "Courier New", monospace;
    font-size: 12.5px;
  }
  .log-entry {
    display: flex;
    padding: 6px 16px;
    border-bottom: 1px solid rgba(48,54,61,.5);
    align-items: flex-start;
    gap: 10px;
    animation: fadeIn .15s ease-out;
  }
  @keyframes fadeIn {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: translateY(0); }
  }
  .log-entry:hover { background: rgba(110,118,129,.08); }
  .log-icon { flex-shrink: 0; width: 16px; text-align: center; }
  .log-time { flex-shrink: 0; color: var(--debug); }
  .log-content { flex: 1; white-space: pre-wrap; word-break: break-all; }
  .log-count {
    flex-shrink: 0;
    background: #30363d;
    color: #fff;
    border-radius: 8px;
    padding: 0 7px;
    font-size: 11px;
    min-width: 18px;
    text-align: center;
  }

  .log-entry.log .log-content { color: var(--log); }
  .log-entry.info .log-content { color: var(--text); }
  .log-entry.warn { background: rgba(210,153,34,.08); }
  .log-entry.warn .log-content { color: var(--warn); }
  .log-entry.error { background: rgba(248,81,73,.08); }
  .log-entry.error .log-content { color: var(--error); }
  .log-entry.debug .log-content { color: var(--debug); }

  .empty-tip {
    text-align: center;
    color: var(--debug);
    padding: 40px 20px;
    font-size: 13px;
  }

  /* 滚动条美化 */
  .console-body::-webkit-scrollbar { width: 8px; }
  .console-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
</style>
</head>
<body>

  <div class="toolbar">
    <h1>🛠 页面内 Console 演示(无需打开 F12)</h1>
    <div class="btn-row">
      <button onclick="console.log('这是一条普通日志', {user: 'Tom', id: 1})">console.log</button>
      <button onclick="console.info('这是一条信息提示')">console.info</button>
      <button onclick="console.warn('这是一条警告:内存占用较高')">console.warn</button>
      <button onclick="console.error('这是一条错误:请求 /api/data 失败 (500)')">console.error</button>
      <button onclick="console.debug('调试信息:当前状态 =', {step: 3})">console.debug</button>
      <button onclick="triggerRealError()">触发运行时错误</button>
      <button onclick="triggerPromiseError()">触发 Promise 异常</button>
      <button class="clear" onclick="clearLogs()">清空日志</button>
    </div>
  </div>

  <div class="console-panel">
    <div class="console-header">
      <span class="dot"></span>
      <span>实时捕获中 · 共 <b id="totalCount">0</b> 条</span>
      <div class="filter-tabs">
        <button class="active" data-filter="all" onclick="setFilter('all', this)">全部</button>
        <button data-filter="log" onclick="setFilter('log', this)">Log</button>
        <button data-filter="info" onclick="setFilter('info', this)">Info</button>
        <button data-filter="warn" onclick="setFilter('warn', this)">Warn</button>
        <button data-filter="error" onclick="setFilter('error', this)">Error</button>
      </div>
    </div>
    <div class="console-body" id="consoleBody">
      <div class="empty-tip" id="emptyTip">暂无日志,点击上方按钮试试,或在真实代码里调用 console.xxx()</div>
    </div>
  </div>

<script>
(function () {
  const consoleBody = document.getElementById('consoleBody');
  const emptyTip = document.getElementById('emptyTip');
  const totalCountEl = document.getElementById('totalCount');

  let currentFilter = 'all';
  let totalCount = 0;

  // 保留原始方法,方便同时输出到真实控制台
  const original = {
    log: console.log.bind(console),
    info: console.info.bind(console),
    warn: console.warn.bind(console),
    error: console.error.bind(console),
    debug: console.debug.bind(console),
  };

  const icons = { log: '▶', info: 'ℹ', warn: '⚠', error: '✖', debug: '●' };

  // 把任意参数格式化成字符串(对象/数组转 JSON,Error 显示堆栈)
  function formatArg(arg) {
    if (arg instanceof Error) {
      return arg.stack || (arg.name + ': ' + arg.message);
    }
    if (typeof arg === 'object' && arg !== null) {
      try {
        return JSON.stringify(arg, null, 2);
      } catch (e) {
        return String(arg);
      }
    }
    return String(arg);
  }

  function getTime() {
    const d = new Date();
    const pad = n => String(n).padStart(2, '0');
    return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3,'0')}`;
  }

  function renderLog(type, args) {
    totalCount++;
    totalCountEl.textContent = totalCount;
    emptyTip.style.display = 'none';

    const text = args.map(formatArg).join(' ');

    // 合并相邻的重复日志(类似真实控制台的折叠计数)
    const lastEntry = consoleBody.lastElementChild;
    if (lastEntry && lastEntry.dataset.type === type && lastEntry.dataset.text === text) {
      const countEl = lastEntry.querySelector('.log-count');
      const n = parseInt(countEl.dataset.n || '1', 10) + 1;
      countEl.dataset.n = n;
      countEl.textContent = n;
      countEl.style.display = 'inline-block';
      return;
    }

    const entry = document.createElement('div');
    entry.className = `log-entry ${type}`;
    entry.dataset.type = type;
    entry.dataset.text = text;
    entry.style.display = (currentFilter === 'all' || currentFilter === type) ? 'flex' : 'none';

    entry.innerHTML = `
      <span class="log-icon">${icons[type] || '▶'}</span>
      <span class="log-time">${getTime()}</span>
      <span class="log-content"></span>
      <span class="log-count" data-n="1" style="display:none">1</span>
    `;
    entry.querySelector('.log-content').textContent = text;

    consoleBody.appendChild(entry);
    consoleBody.scrollTop = consoleBody.scrollHeight;
  }

  // 重写 console 各方法
  ['log', 'info', 'warn', 'error', 'debug'].forEach(type => {
    console[type] = function (...args) {
      original[type](...args);   // 仍然输出到真实 F12 控制台
      renderLog(type, args);      // 同时渲染到页面
    };
  });

  // 捕获未被 try/catch 的运行时错误
  window.addEventListener('error', function (e) {
    renderLog('error', [`未捕获异常: ${e.message} (${e.filename}:${e.lineno}:${e.colno})`]);
  });

  // 捕获未处理的 Promise rejection
  window.addEventListener('unhandledrejection', function (e) {
    renderLog('error', [`未处理的 Promise 异常: ${formatArg(e.reason)}`]);
  });

  // 过滤器
  window.setFilter = function (filter, btn) {
    currentFilter = filter;
    document.querySelectorAll('.filter-tabs button').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    document.querySelectorAll('.log-entry').forEach(el => {
      el.style.display = (filter === 'all' || el.dataset.type === filter) ? 'flex' : 'none';
    });
  };

  window.clearLogs = function () {
    consoleBody.innerHTML = '';
    consoleBody.appendChild(emptyTip);
    emptyTip.style.display = 'block';
    totalCount = 0;
    totalCountEl.textContent = 0;
  };

  window.triggerRealError = function () {
    // 故意调用一个不存在的函数,产生真实的运行时错误
    nonExistentFunction();
  };

  window.triggerPromiseError = function () {
    Promise.reject(new Error('模拟的异步请求失败'));
  };

  // 页面加载时打一条欢迎日志,验证功能生效
  console.log('✅ Console 已接管,日志将同时显示在页面与 F12 中');
})();
</script>

</body>
</html>
相关推荐
在书中成长1 小时前
HarmonyOS 小游戏《对战五子棋》开发第17篇 - AI的两种评估策略
android·java·javascript
甜味弥漫1 小时前
后端开发入门:业务能力、数据库与程序员的核心竞争力
javascript·node.js·全栈
湮w1 小时前
JavaWeb(三)JavaScript详细讲解
java·javascript·web
成都渲染101云渲染66669 小时前
如何在3ds Max中实现更快、更高质量的渲染
前端·javascript·人工智能
SRET13 小时前
Mineradio 沙盒隔离安装完全指南 | 一键安全运行 Electron 应用!!!
javascript·经验分享·安全·electron·aigc·音视频·ai编程
全栈前端老曹13 小时前
【MongoDB】安全与权限管理 —— 用户认证、角色权限、SSL 加密
前端·javascript·数据库·安全·mongodb·nosql·ssl
言乐614 小时前
Python快速生成工作报告
python·系统架构·html·交互·软件构建
free3516 小时前
作用域链与 Environment:变量查找、遮蔽和赋值怎么实现
javascript
YHL16 小时前
🤖 Agent 智能体开发实战 · 第一课:Tool Use —— 让大模型自动干活
前端·javascript·人工智能