敢测 AI 服务现场

← 返回首页

同题现场

代码能力题现场

同一道题,多个 AI 服务一起交卷。这里不做排名,直接看输出、错误和现场标签。

单文件 HTML:番茄任务看板

看完整现场

OfoxAI

ofoxClaudeOpus4.8 · anthropic/claude-opus-4.8

Succeeded

2026-05-30 15:45:10 · 86273 ms · 输入 653 / 输出 9,760 / 总计 10,413 tokens

查看完整源码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>番茄任务看板</title>
<style>
  :root {
    --bg: #f4f5f7;
    --panel: #ffffff;
    --border: #e2e5e9;
    --text: #1f2733;
    --muted: #6b7280;
    --accent: #c0392b;
    --accent-soft: #fdecea;
    --todo: #5a7184;
    --doing: #d68910;
    --done: #1e8449;
    --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.5;
    font-size: 14px;
  }
  header.app-header {
    background: var(--panel);
    border-bottom: 1px solid var(--border);
    padding: 14px 18px;
    position: sticky;
    top: 0;
    z-index: 20;
  }
  .header-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
  }
  .header-top h1 {
    font-size: 18px;
    margin: 0;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .header-top h1 .dot {
    width: 12px; height: 12px; border-radius: 50%;
    background: var(--accent); display: inline-block;
  }
  .toggle-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    color: var(--muted);
  }
  .switch {
    position: relative; display: inline-block; width: 42px; height: 22px;
  }
  .switch input { opacity: 0; width: 0; height: 0; }
  .slider {
    position: absolute; cursor: pointer; inset: 0;
    background: #cbd2d9; transition: .2s; border-radius: 22px;
  }
  .slider:before {
    position: absolute; content: ""; height: 16px; width: 16px;
    left: 3px; bottom: 3px; background: #fff; transition: .2s; border-radius: 50%;
  }
  .switch input:checked + .slider { background: var(--accent); }
  .switch input:checked + .slider:before { transform: translateX(20px); }

  .controls {
    margin-top: 12px;
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
  }
  .controls input[type="search"] {
    flex: 1 1 200px;
    min-width: 160px;
  }
  input, select, button, textarea {
    font: inherit;
  }
  input[type="text"], input[type="search"], input[type="number"], select {
    padding: 8px 10px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fff;
    color: var(--text);
    outline: none;
  }
  input:focus, select:focus { border-color: var(--accent); }
  button {
    cursor: pointer;
    border: 1px solid var(--border);
    background: #fff;
    color: var(--text);
    padding: 8px 12px;
    border-radius: 6px;
    transition: background .15s, border-color .15s;
  }
  button:hover { background: #f0f1f3; }
  button.primary {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
  }
  button.primary:hover { background: #a93226; }
  button.danger {
    color: var(--accent);
    border-color: #e8b7b1;
  }
  button.danger:hover { background: var(--accent-soft); }

  main {
    padding: 16px;
    max-width: 1100px;
    margin: 0 auto;
  }
  .board {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 14px;
  }
  .column {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    display: flex;
    flex-direction: column;
    min-height: 200px;
  }
  .column.drag-over {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
  }
  .col-head {
    padding: 10px 12px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-weight: 600;
    font-size: 13px;
  }
  .col-head .label { display: flex; align-items: center; gap: 7px; }
  .col-head .bar { width: 10px; height: 10px; border-radius: 3px; }
  .col-todo .bar { background: var(--todo); }
  .col-doing .bar { background: var(--doing); }
  .col-done .bar { background: var(--done); }
  .count {
    background: var(--bg);
    color: var(--muted);
    border-radius: 10px;
    padding: 1px 8px;
    font-size: 12px;
    font-weight: 600;
  }
  .col-body {
    padding: 10px;
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  .empty {
    color: var(--muted);
    font-size: 12.5px;
    text-align: center;
    padding: 24px 8px;
    border: 1px dashed var(--border);
    border-radius: 8px;
    background: #fafbfc;
  }
  .card {
    background: #fff;
    border: 1px solid var(--border);
    border-left-width: 4px;
    border-radius: 8px;
    padding: 10px 11px;
    box-shadow: var(--shadow);
    cursor: grab;
  }
  .card.dragging { opacity: .5; }
  .card.prio-high { border-left-color: var(--accent); }
  .card.prio-mid { border-left-color: var(--doing); }
  .card.prio-low { border-left-color: var(--todo); }
  .card-title {
    font-weight: 600;
    font-size: 14px;
    word-break: break-word;
    margin-bottom: 6px;
  }
  .card-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 8px;
  }
  .tag {
    font-size: 11px;
    padding: 2px 7px;
    border-radius: 10px;
    background: var(--bg);
    color: var(--muted);
  }
  .tag.high { background: var(--accent-soft); color: var(--accent); }
  .tag.mid { background: #fef5e7; color: #b9770e; }
  .tag.low { background: #eef2f5; color: var(--todo); }
  .timer-line {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 8px;
  }
  .timer-display {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    font-size: 16px;
    letter-spacing: 1px;
  }
  .timer-display.running { color: var(--done); }
  .timer-display.paused { color: var(--doing); }
  .timer-done { color: var(--accent); }
  .card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
  }
  .card-actions button {
    padding: 5px 9px;
    font-size: 12px;
  }
  .card-actions .del { margin-left: auto; }

  /* modal */
  .overlay {
    position: fixed; inset: 0;
    background: rgba(20,25,33,0.45);
    display: none;
    align-items: center;
    justify-content: center;
    padding: 16px;
    z-index: 50;
  }
  .overlay.show { display: flex; }
  .modal {
    background: #fff;
    border-radius: 12px;
    width: 100%;
    max-width: 420px;
    padding: 18px;
    box-shadow: 0 10px 40px rgba(0,0,0,.25);
  }
  .modal h2 { margin: 0 0 14px; font-size: 16px; }
  .field { margin-bottom: 12px; }
  .field label { display: block; font-size: 12.5px; color: var(--muted); margin-bottom: 4px; }
  .field input, .field select { width: 100%; }
  .err {
    color: var(--accent);
    font-size: 12px;
    margin-top: 4px;
    display: none;
  }
  .err.show { display: block; }
  .modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 6px;
  }
  .fab {
    position: fixed;
    right: 18px;
    bottom: 18px;
    z-index: 30;
  }
  .fab button {
    border-radius: 28px;
    padding: 12px 18px;
    font-weight: 600;
    box-shadow: 0 4px 14px rgba(192,57,43,.35);
  }
  @media (max-width: 760px) {
    .board { grid-template-columns: 1fr; }
    .header-top h1 { font-size: 16px; }
  }
</style>
</head>
<body>
<header class="app-header">
  <div class="header-top">
    <h1><span class="dot"></span>番茄任务看板</h1>
    <label class="toggle-wrap" title="开启后 25 分钟按 25 秒运行">
      <span>演示加速</span>
      <span class="switch">
        <input type="checkbox" id="turboToggle">
        <span class="slider"></span>
      </span>
    </label>
  </div>
  <div class="controls">
    <input type="search" id="searchInput" placeholder="按标题搜索任务…" autocomplete="off">
    <button class="primary" id="addBtn">+ 新增任务</button>
    <button class="danger" id="clearDoneBtn">清空已完成</button>
  </div>
</header>

<main>
  <div class="board">
    <section class="column col-todo" data-status="todo">
      <div class="col-head">
        <span class="label"><span class="bar"></span>待办</span>
        <span class="count" data-count>0</span>
      </div>
      <div class="col-body" data-body></div>
    </section>
    <section class="column col-doing" data-status="doing">
      <div class="col-head">
        <span class="label"><span class="bar"></span>进行中</span>
        <span class="count" data-count>0</span>
      </div>
      <div class="col-body" data-body></div>
    </section>
    <section class="column col-done" data-status="done">
      <div class="col-head">
        <span class="label"><span class="bar"></span>已完成</span>
        <span class="count" data-count>0</span>
      </div>
      <div class="col-body" data-body></div>
    </section>
  </div>
</main>

<div class="fab">
  <button class="primary" id="fabBtn">+ 任务</button>
</div>

<div class="overlay" id="overlay">
  <div class="modal">
    <h2>新增任务</h2>
    <form id="taskForm" novalidate>
      <div class="field">
        <label for="fTitle">任务标题</label>
        <input type="text" id="fTitle" maxlength="80" placeholder="例如:实现登录接口">
        <div class="err" id="errTitle">请输入任务标题。</div>
      </div>
      <div class="field">
        <label for="fPomo">预估番茄数</label>
        <input type="number" id="fPomo" min="1" max="20" value="1">
        <div class="err" id="errPomo">请输入 1–20 之间的整数。</div>
      </div>
      <div class="field">
        <label for="fPrio">优先级</label>
        <select id="fPrio">
          <option value="high">高</option>
          <option value="mid" selected>中</option>
          <option value="low">低</option>
        </select>
      </div>
      <div class="modal-actions">
        <button type="button" id="cancelBtn">取消</button>
        <button type="submit" class="primary">保存</button>
      </div>
    </form>
  </div>
</div>

<script>
(function () {
  "use strict";

  var STORAGE_KEY = "pomodoroBoard.tasks.v1";
  var TURBO_KEY = "pomodoroBoard.turbo.v1";
  var FULL_SECONDS = 25 * 60;
  var TURBO_SECONDS = 25;

  var tasks = load();
  var turbo = loadTurbo();
  var searchTerm = "";
  var timers = {}; // id -> { intervalId }

  var prioLabel = { high: "高优先级", mid: "中优先级", low: "低优先级" };
  var prioClass = { high: "high", mid: "mid", low: "low" };
  var statusList = ["todo", "doing", "done"];

  // ---- persistence ----
  function load() {
    try {
      var raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return [];
      var arr = JSON.parse(raw);
      if (!Array.isArray(arr)) return [];
      return arr.map(normalize);
    } catch (e) {
      return [];
    }
  }
  function normalize(t) {
    return {
      id: t.id || genId(),
      title: String(t.title || "未命名任务"),
      pomo: clampInt(t.pomo, 1, 20, 1),
      prio: ["high", "mid", "low"].indexOf(t.prio) >= 0 ? t.prio : "mid",
      status: statusList.indexOf(t.status) >= 0 ? t.status : "todo",
      remaining: typeof t.remaining === "number" ? t.remaining : null,
      timerState: t.timerState === "running" || t.timerState === "paused" ? t.timerState : "idle",
      finished: !!t.finished
    };
  }
  function save() {
    try {
      // persist only serializable, never store running interval handles
      var clean = tasks.map(function (t) {
        return {
          id: t.id, title: t.title, pomo: t.pomo, prio: t.prio,
          status: t.status, remaining: t.remaining,
          timerState: t.timerState === "running" ? "paused" : t.timerState,
          finished: t.finished
        };
      });
      localStorage.setItem(STORAGE_KEY, JSON.stringify(clean));
    } catch (e) {}
  }
  function loadTurbo() {
    try { return localStorage.getItem(TURBO_KEY) === "1"; } catch (e) { return false; }
  }
  function saveTurbo() {
    try { localStorage.setItem(TURBO_KEY, turbo ? "1" : "0"); } catch (e) {}
  }

  function clampInt(v, min, max, fallback) {
    var n = parseInt(v, 10);
    if (isNaN(n)) return fallback;
    if (n < min) return min;
    if (n > max) return max;
    return n;
  }
  function genId() {
    return "t" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
  }
  function defaultDuration() {
    return turbo ? TURBO_SECONDS : FULL_SECONDS;
  }

  // ---- DOM refs ----
  var overlay = document.getElementById("overlay");
  var form = document.getElementById("taskForm");
  var fTitle = document.getElementById("fTitle");
  var fPomo = document.getElementById("fPomo");
  var fPrio = document.getElementById("fPrio");
  var errTitle = document.getElementById("errTitle");
  var errPomo = document.getElementById("errPomo");
  var searchInput = document.getElementById("searchInput");
  var turboToggle = document.getElementById("turboToggle");

  turboToggle.checked = turbo;

  // ---- modal ----
  function openModal() {
    form.reset();
    fPomo.value = "1";
    fPrio.value = "mid";
    errTitle.classList.remove("show");
    errPomo.classList.remove("show");
    overlay.classList.add("show");
    setTimeout(function () { fTitle.focus(); }, 50);
  }
  function closeModal() {
    overlay.classList.remove("show");
  }

  document.getElementById("addBtn").addEventListener("click", openModal);
  document.getElementById("fabBtn").addEventListener("click", openModal);
  document.getElementById("cancelBtn").addEventListener("click", closeModal);
  overlay.addEventListener("click", function (e) {
    if (e.target === overlay) closeModal();
  });
  document.addEventListener("keydown", function (e) {
    if (e.key === "Escape" && overlay.classList.contains("show")) closeModal();
  });

  form.addEventListener("submit", function (e) {
    e.preventDefault();
    var title = fTitle.value.trim();
    var ok = true;
    errTitle.classList.remove("show");
    errPomo.classList.remove("show");
    if (!title) { errTitle.classList.add("show"); ok = false; }
    var pomoRaw = fPomo.value.trim();
    var pomoNum = parseInt(pomoRaw, 10);
    if (pomoRaw === "" || isNaN(pomoNum) || pomoNum < 1 || pomoNum > 20 || String(pomoNum) !== pomoRaw) {
      errPomo.classList.add("show"); ok = false;
    }
    if (!ok) return;

    tasks.push({
      id: genId(),
      title: title,
      pomo: pomoNum,
      prio: fPrio.value,
      status: "todo",
      remaining: null,
      timerState: "idle",
      finished: false
    });
    save();
    closeModal();
    render();
  });

  // ---- search ----
  searchInput.addEventListener("input", function () {
    searchTerm = searchInput.value.trim().toLowerCase();
    render();
  });

  // ---- turbo ----
  turboToggle.addEventListener("change", function () {
    turbo = turboToggle.checked;
    saveTurbo();
    // reset idle timers' duration baseline (only affects future starts)
    render();
  });

  // ---- clear done ----
  document.getElementById("clearDoneBtn").addEventListener("click", function () {
    var doneCount = tasks.filter(function (t) { return t.status === "done"; }).length;
    if (doneCount === 0) {
      alert("当前没有已完成任务可清空。");
      return;
    }
    if (!confirm("确定要清空 " + doneCount + " 个已完成任务吗?")) return;
    tasks.filter(function (t) { return t.status === "done"; }).forEach(function (t) { stopTimer(t.id); });
    tasks = tasks.filter(function (t) { return t.status !== "done"; });
    save();
    render();
  });

  // ---- timer logic ----
  function startTimer(id) {
    var t = findTask(id);
    if (!t) return;
    if (t.remaining === null || t.finished) {
      t.remaining = defaultDuration();
      t.finished = false;
    }
    if (t.remaining <= 0) {
      t.remaining = defaultDuration();
      t.finished = false;
    }
    t.timerState = "running";
    runInterval(id);
    save();
    render();
  }
  function runInterval(id) {
    stopInterval(id);
    timers[id] = setInterval(function () {
      var t = findTask(id);
      if (!t || t.timerState !== "running") { stopInterval(id); return; }
      t.remaining -= 1;
      if (t.remaining <= 0) {
        t.remaining = 0;
        t.timerState = "idle";
        t.finished = true;
        stopInterval(id);
        save();
        render();
      } else {
        updateTimerDisplay(t);
      }
    }, 1000);
  }
  function pauseTimer(id) {
    var t = findTask(id);
    if (!t) return;
    t.timerState = "paused";
    stopInterval(id);
    save();
    render();
  }
  function resetTimer(id) {
    var t = findTask(id);
    if (!t) return;
    stopInterval(id);
    t.remaining = defaultDuration();
    t.timerState = "idle";
    t.finished = false;
    save();
    render();
  }
  function stopTimer(id) {
    var t = findTask(id);
    if (t) { t.timerState = "idle"; }
    stopInterval(id);
  }
  function stopInterval(id) {
    if (timers[id]) {
      clearInterval(timers[id]);
      delete timers[id];
    }
  }
  function findTask(id) {
    for (var i = 0; i < tasks.length; i++) if (tasks[i].id === id) return tasks[i];
    return null;
  }
  function formatTime(s) {
    if (s === null || s === undefined) s = defaultDuration();
    s = Math.max(0, s);
    var m = Math.floor(s / 60);
    var sec = s % 60;
    return (m < 10 ? "0" + m : m) + ":" + (sec < 10 ? "0" + sec : sec);
  }
  function updateTimerDisplay(t) {
    var el = document.querySelector('[data-timer="' + t.id + '"]');
    if (el) el.textContent = formatTime(t.remaining);
  }

  // ---- drag & drop ----
  var draggingId = null;

  function attachColumnDnD() {
    document.querySelectorAll(".column").forEach(function (col) {
      col.addEventListener("dragover", function (e) {
        e.preventDefault();
        col.classList.add("drag-over");
      });
      col.addEventListener("dragleave", function () {
        col.classList.remove("drag-over");
      });
      col.addEventListener("drop", function (e) {
        e.preventDefault();
        col.classList.remove("drag-over");
        if (!draggingId) return;
        var newStatus = col.getAttribute("data-status");
        var t = findTask(draggingId);
        if (t && t.status !== newStatus) {
          t.status = newStatus;
          save();
          render();
        }
        draggingId = null;
      });
    });
  }

  // ---- rendering ----
  function render() {
    var counts = { todo: 0, doing: 0, done: 0 };
    var bodies = {};
    statusList.forEach(function (s) {
      var col = document.querySelector('.column[data-status="' + s + '"]');
      bodies[s] = col.querySelector("[data-body]");
      bodies[s].innerHTML = "";
    });

    var term = searchTerm;
    var totalMatch = { todo: 0, doing: 0, done: 0 };

    tasks.forEach(function (t) {
      counts[t.status] += 1;
      var matches = !term || t.title.toLowerCase().indexOf(term) >= 0;
      if (matches) {
        totalMatch[t.status] += 1;
        bodies[t.status].appendChild(buildCard(t));
      }
    });

    statusList.forEach(function (s) {
      var col = document.querySelector('.column[data-status="' + s + '"]');
      col.querySelector("[data-count]").textContent = counts[s];
      if (totalMatch[s] === 0) {
        var msg;
        if (counts[s] > 0 && term) {
          msg = "没有匹配“" + term + "”的任务。";
        } else if (s === "todo") {
          msg = "暂无待办任务,点击右下角“+ 任务”添加。";
        } else if (s === "doing") {
          msg = "拖拽任务到这里开始处理。";
        } else {
          msg = "完成的任务会出现在这里。";
        }
        var empty = document.createElement("div");
        empty.className = "empty";
        empty.textContent = msg;
        bodies[s].appendChild(empty);
      }
    });
  }

  function buildCard(t) {
    var card = document.createElement("div");
    card.className = "card prio-" + (t.prio === "high" ? "high" : t.prio === "low" ? "low" : "mid");
    card.setAttribute("draggable", "true");
    card.setAttribute("data-id", t.id);

    card.addEventListener("dragstart", function () {
      draggingId = t.id;
      card.classList.add("dragging");
    });
    card.addEventListener("dragend", function () {
      card.classList.remove("dragging");
    });

    var title = document.createElement("div");
    title.className = "card-title";
    title.textContent = t.title;
    card.appendChild(title);

    var meta = document.createElement("div");
    meta.className = "card-meta";
    var pTag = document.createElement("span");
    pTag.className = "tag " + prioClass[t.prio];
    pTag.textContent = prioLabel[t.prio];
    meta.appendChild(pTag);
    var pomoTag = document.createElement("span");
    pomoTag.className = "tag";
    pomoTag.textContent = "???? " + t.pomo + " 个";
    meta.appendChild(pomoTag);
    card.appendChild(meta);

    // timer
    if (t.timerState !== "idle" || t.finished || (t.remaining !== null && t.remaining > 0)) {
      var line = document.createElement("div");
      line.className = "timer-line";
      var disp = document.createElement("span");
      disp.className = "timer-display" + (t.timerState === "running" ? " running" : t.timerState === "paused" ? " paused" : (t.finished ? " timer-done" : ""));
      disp.setAttribute("data-timer", t.id);
      disp.textContent = t.finished ? "完成" : formatTime(t.remaining);
      line.appendChild(disp);
      var statusTxt = document.createElement("span");
      statusTxt.className = "tag";
      statusTxt.textContent = t.timerState === "running" ? "进行中" : t.timerState === "paused" ? "已暂停" : (t.finished ? "番茄已完成" : "");
      line.appendChild(statusTxt);
      card.appendChild(line);
    }

    var actions = document.createElement("div");
    actions.className = "card-actions";

    if (t.timerState === "running") {
      actions.appendChild(btn("暂停", function () { pauseTimer(t.id); }));
      actions.appendChild(btn("重置", function () { resetTimer(t.id); }));
    } else if (t.timerState === "paused") {
      actions.appendChild(btn("继续", function () { startTimer(t.id); }, "primary"));
      actions.appendChild(btn("重置", function () { resetTimer(t.id); }));
    } else {
      actions.appendChild(btn(t.finished ? "再来一次" : "开始 25 分钟", function () { startTimer(t.id); }, "primary"));
      if (t.finished) actions.appendChild(btn("重置", function () { resetTimer(t.id); }));
    }

    var del = btn("删除", function () {
      if (confirm("删除任务“" + t.title + "”?")) {
        stopTimer(t.id);
        tasks = tasks.filter(function (x) { return x.id !== t.id; });
        save();
        render();
      }
    }, "danger");
    del.classList.add("del");
    actions.appendChild(del);

    card.appendChild(actions);
    return card;
  }

  function btn(label, handler, cls) {
    var b = document.createElement("button");
    b.type = "button";
    b.textContent = label;
    if (cls) b.className = cls;
    b.addEventListener("click", function (e) {
      e.stopPropagation();
      handler();
    });
    return b;
  }

  // ---- resume timers that were running (now persisted as paused) ----
  // On load anything previously "running" becomes "paused" via save normalization,
  // but in-memory we may want to keep them paused. They start paused. Good.

  attachColumnDnD();
  render();

  window.addEventListener("beforeunload", save);
})();
</script>
</body>
</html>
有真实输出 完整 HTML 包含交互脚本 无外部资源 无 Markdown 包裹

OfoxAI

ofoxOpenAIGPT5.5 · openai/gpt-5.5

Succeeded

2026-05-23 18:20:56 · 127053 ms · 输入 422 / 输出 11,280 / 总计 11,702 tokens

查看完整源码
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>番茄任务看板</title>
  <style>
    :root {
      --bg: #f6f7f9;
      --panel: #ffffff;
      --panel-2: #f9fafb;
      --text: #172033;
      --muted: #667085;
      --border: #d9dee7;
      --primary: #2855c7;
      --primary-hover: #2146a5;
      --danger: #b42318;
      --danger-bg: #fff1f0;
      --success: #027a48;
      --success-bg: #ecfdf3;
      --warning: #b54708;
      --warning-bg: #fffaeb;
      --shadow: 0 10px 30px rgba(16, 24, 40, 0.08);
      --radius: 14px;
      --radius-sm: 10px;
      --focus: 0 0 0 3px rgba(40, 85, 199, 0.18);
    }

    * {
      box-sizing: border-box;
    }

    html {
      min-height: 100%;
      background: var(--bg);
    }

    body {
      margin: 0;
      min-height: 100%;
      color: var(--text);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
      line-height: 1.5;
    }

    button,
    input,
    select {
      font: inherit;
    }

    button {
      border: 0;
      cursor: pointer;
    }

    button:disabled {
      cursor: not-allowed;
      opacity: 0.55;
    }

    .app {
      width: min(1180px, calc(100% - 32px));
      margin: 0 auto;
      padding: 28px 0 36px;
    }

    .topbar {
      display: flex;
      gap: 18px;
      align-items: flex-start;
      justify-content: space-between;
      margin-bottom: 18px;
    }

    .title-block h1 {
      margin: 0;
      font-size: clamp(24px, 4vw, 34px);
      letter-spacing: -0.03em;
    }

    .title-block p {
      margin: 6px 0 0;
      color: var(--muted);
      font-size: 15px;
    }

    .toolbar {
      display: flex;
      gap: 10px;
      align-items: center;
      flex-wrap: wrap;
      justify-content: flex-end;
    }

    .search {
      width: min(320px, 100%);
      position: relative;
    }

    .search input {
      width: 100%;
      height: 42px;
      padding: 0 12px;
      border: 1px solid var(--border);
      border-radius: 999px;
      background: var(--panel);
      color: var(--text);
      outline: none;
    }

    .search input:focus,
    .form-grid input:focus,
    .form-grid select:focus,
    .move-select:focus {
      border-color: var(--primary);
      box-shadow: var(--focus);
    }

    .switch {
      display: inline-flex;
      align-items: center;
      gap: 9px;
      padding: 9px 12px;
      border: 1px solid var(--border);
      border-radius: 999px;
      background: var(--panel);
      color: var(--text);
      user-select: none;
      white-space: nowrap;
      font-size: 14px;
    }

    .switch input {
      appearance: none;
      width: 38px;
      height: 22px;
      border-radius: 999px;
      background: #cbd5e1;
      position: relative;
      outline: none;
      cursor: pointer;
      transition: background 0.18s ease;
    }

    .switch input::after {
      content: "";
      width: 18px;
      height: 18px;
      border-radius: 50%;
      background: #fff;
      position: absolute;
      left: 2px;
      top: 2px;
      transition: transform 0.18s ease;
      box-shadow: 0 1px 3px rgba(16, 24, 40, 0.25);
    }

    .switch input:checked {
      background: var(--primary);
    }

    .switch input:checked::after {
      transform: translateX(16px);
    }

    .switch input:focus {
      box-shadow: var(--focus);
    }

    .panel {
      background: var(--panel);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      box-shadow: var(--shadow);
    }

    .task-form {
      padding: 18px;
      margin-bottom: 18px;
    }

    .form-grid {
      display: grid;
      grid-template-columns: minmax(220px, 1fr) 150px 150px auto;
      gap: 12px;
      align-items: end;
    }

    .field label {
      display: block;
      margin-bottom: 6px;
      font-weight: 650;
      font-size: 13px;
      color: #344054;
    }

    .field input,
    .field select {
      width: 100%;
      height: 42px;
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      padding: 0 11px;
      outline: none;
      background: #fff;
      color: var(--text);
    }

    .primary-btn,
    .secondary-btn,
    .danger-btn,
    .ghost-btn {
      min-height: 38px;
      border-radius: 10px;
      padding: 8px 12px;
      font-weight: 650;
      transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.08s ease;
    }

    .primary-btn {
      height: 42px;
      color: #fff;
      background: var(--primary);
    }

    .primary-btn:hover:not(:disabled) {
      background: var(--primary-hover);
    }

    .secondary-btn {
      color: var(--primary);
      background: #eef3ff;
      border: 1px solid #c7d7fe;
    }

    .secondary-btn:hover:not(:disabled) {
      background: #e1eaff;
    }

    .danger-btn {
      color: var(--danger);
      background: var(--danger-bg);
      border: 1px solid #fecaca;
    }

    .danger-btn:hover:not(:disabled) {
      background: #ffe4e1;
    }

    .ghost-btn {
      color: #344054;
      background: #fff;
      border: 1px solid var(--border);
    }

    .ghost-btn:hover:not(:disabled) {
      background: #f8fafc;
    }

    .message {
      display: none;
      margin-top: 12px;
      padding: 10px 12px;
      border-radius: 10px;
      font-size: 14px;
    }

    .message.show {
      display: block;
    }

    .message.error {
      color: var(--danger);
      background: var(--danger-bg);
      border: 1px solid #fecaca;
    }

    .message.success {
      color: var(--success);
      background: var(--success-bg);
      border: 1px solid #abefc6;
    }

    .message.info {
      color: #175cd3;
      background: #eff6ff;
      border: 1px solid #bfdbfe;
    }

    .app-message {
      margin-bottom: 14px;
    }

    .board {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 14px;
      align-items: start;
    }

    .column {
      min-height: 360px;
      background: rgba(255, 255, 255, 0.74);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      overflow: hidden;
      transition: border-color 0.15s ease, background 0.15s ease;
    }

    .column.drag-over {
      border-color: var(--primary);
      background: #f0f5ff;
    }

    .column-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      padding: 14px 14px 12px;
      border-bottom: 1px solid var(--border);
      background: #fff;
    }

    .column-title {
      display: flex;
      align-items: center;
      gap: 8px;
      min-width: 0;
    }

    .column-title h2 {
      margin: 0;
      font-size: 16px;
      letter-spacing: -0.01em;
    }

    .count {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 25px;
      height: 24px;
      padding: 0 8px;
      border-radius: 999px;
      background: #eef2f7;
      color: #475467;
      font-size: 12px;
      font-weight: 700;
    }

    .card-list {
      display: grid;
      gap: 10px;
      padding: 12px;
      min-height: 290px;
    }

    .empty {
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 160px;
      padding: 18px;
      border: 1px dashed #cbd5e1;
      border-radius: var(--radius-sm);
      color: var(--muted);
      text-align: center;
      font-size: 14px;
      background: rgba(255, 255, 255, 0.55);
    }

    .card {
      background: #fff;
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      padding: 12px;
      box-shadow: 0 3px 10px rgba(16, 24, 40, 0.05);
      transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease, opacity 0.12s ease;
    }

    .card:hover {
      box-shadow: 0 8px 20px rgba(16, 24, 40, 0.08);
    }

    .card.dragging {
      opacity: 0.55;
      transform: scale(0.99);
    }

    .card.priority-high {
      border-left: 4px solid #d92d20;
    }

    .card.priority-medium {
      border-left: 4px solid #f79009;
    }

    .card.priority-low {
      border-left: 4px solid #12b76a;
    }

    .card-top {
      display: flex;
      gap: 10px;
      align-items: flex-start;
      justify-content: space-between;
    }

    .card h3 {
      margin: 0;
      font-size: 15px;
      line-height: 1.35;
      word-break: break-word;
    }

    .pill {
      flex: 0 0 auto;
      display: inline-flex;
      align-items: center;
      height: 24px;
      padding: 0 8px;
      border-radius: 999px;
      font-size: 12px;
      font-weight: 700;
      white-space: nowrap;
    }

    .pill.high {
      color: #b42318;
      background: #fee4e2;
    }

    .pill.medium {
      color: #b54708;
      background: #fef0c7;
    }

    .pill.low {
      color: #027a48;
      background: #d1fadf;
    }

    .meta {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin: 10px 0;
      color: var(--muted);
      font-size: 13px;
    }

    .timer {
      margin-top: 10px;
      padding: 10px;
      border-radius: 10px;
      background: var(--panel-2);
      border: 1px solid #edf0f5;
    }

    .timer-row {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 8px;
      margin-bottom: 9px;
    }

    .timer-time {
      font-variant-numeric: tabular-nums;
      font-size: 24px;
      font-weight: 800;
      letter-spacing: -0.03em;
    }

    .timer-state {
      color: var(--muted);
      font-size: 12px;
      white-space: nowrap;
    }

    .timer-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 7px;
    }

    .timer-actions button {
      min-height: 34px;
      padding: 7px 9px;
      border-radius: 9px;
      font-size: 13px;
    }

    .card-footer {
      display: flex;
      gap: 8px;
      align-items: center;
      justify-content: space-between;
      margin-top: 10px;
      padding-top: 10px;
      border-top: 1px solid #edf0f5;
    }

    .drag-hint {
      color: var(--muted);
      font-size: 12px;
    }

    .move-select {
      max-width: 116px;
      height: 34px;
      border: 1px solid var(--border);
      border-radius: 9px;
      background: #fff;
      color: #344054;
      padding: 0 8px;
      outline: none;
      font-size: 13px;
    }

    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    }

    @media (max-width: 900px) {
      .topbar {
        flex-direction: column;
      }

      .toolbar {
        width: 100%;
        justify-content: flex-start;
      }

      .search {
        width: 100%;
      }

      .form-grid {
        grid-template-columns: 1fr 1fr;
      }

      .form-grid .title-field {
        grid-column: 1 / -1;
      }

      .form-grid .submit-field {
        grid-column: 1 / -1;
      }

      .form-grid .submit-field button {
        width: 100%;
      }

      .board {
        grid-template-columns: 1fr;
      }

      .column {
        min-height: auto;
      }
    }

    @media (max-width: 430px) {
      .app {
        width: min(100% - 20px, 1180px);
        padding-top: 18px;
      }

      .task-form,
      .column-header {
        padding: 12px;
      }

      .form-grid {
        grid-template-columns: 1fr;
      }

      .toolbar {
        gap: 8px;
      }

      .switch {
        width: 100%;
        justify-content: space-between;
      }

      .card-list {
        padding: 10px;
      }

      .card-footer {
        align-items: stretch;
        flex-direction: column;
      }

      .move-select {
        max-width: none;
        width: 100%;
      }

      .timer-actions button {
        flex: 1 1 auto;
      }
    }
  </style>
</head>
<body>
  <div class="app">
    <header class="topbar">
      <div class="title-block">
        <h1>番茄任务看板</h1>
        <p>管理今天的开发任务,拖拽卡片更新状态,并用 25 分钟番茄钟保持专注。</p>
      </div>
      <div class="toolbar" aria-label="看板工具">
        <label class="search">
          <span class="sr-only">按标题搜索任务</span>
          <input id="searchInput" type="search" placeholder="按标题搜索任务">
        </label>
        <label class="switch" title="打开后,25 分钟番茄钟会按 25 秒倒计时,便于演示">
          <span>演示加速</span>
          <input id="demoToggle" type="checkbox" role="switch" aria-label="演示加速开关">
        </label>
      </div>
    </header>

    <div id="appMessage" class="message app-message" role="status" aria-live="polite"></div>

    <section class="panel task-form" aria-labelledby="newTaskTitle">
      <h2 id="newTaskTitle" class="sr-only">新增任务</h2>
      <form id="taskForm" novalidate>
        <div class="form-grid">
          <div class="field title-field">
            <label for="taskTitle">任务标题</label>
            <input id="taskTitle" name="taskTitle" type="text" maxlength="80" autocomplete="off" placeholder="例如:实现登录接口错误处理">
          </div>
          <div class="field">
            <label for="taskPomodoros">预估番茄数</label>
            <input id="taskPomodoros" name="taskPomodoros" type="number" min="1" max="20" step="1" inputmode="numeric" value="1">
          </div>
          <div class="field">
            <label for="taskPriority">优先级</label>
            <select id="taskPriority" name="taskPriority">
              <option value="high">高</option>
              <option value="medium" selected>中</option>
              <option value="low">低</option>
            </select>
          </div>
          <div class="field submit-field">
            <button class="primary-btn" type="submit">新增任务</button>
          </div>
        </div>
        <div id="formMessage" class="message" role="alert" aria-live="assertive"></div>
      </form>
    </section>

    <main class="board" aria-label="番茄任务看板">
      <section class="column" data-status="todo" aria-labelledby="todoTitle">
        <div class="column-header">
          <div class="column-title">
            <h2 id="todoTitle">待办</h2>
            <span id="todoCount" class="count">0</span>
          </div>
        </div>
        <div id="todoList" class="card-list" data-status="todo"></div>
      </section>

      <section class="column" data-status="doing" aria-labelledby="doingTitle">
        <div class="column-header">
          <div class="column-title">
            <h2 id="doingTitle">进行中</h2>
            <span id="doingCount" class="count">0</span>
          </div>
        </div>
        <div id="doingList" class="card-list" data-status="doing"></div>
      </section>

      <section class="column" data-status="done" aria-labelledby="doneTitle">
        <div class="column-header">
          <div class="column-title">
            <h2 id="doneTitle">已完成</h2>
            <span id="doneCount" class="count">0</span>
          </div>
          <button id="clearDoneBtn" class="danger-btn" type="button">清空已完成</button>
        </div>
        <div id="doneList" class="card-list" data-status="done"></div>
      </section>
    </main>
  </div>

  <script>
    (function () {
      "use strict";

      var STORAGE_KEY = "pomodoro-kanban-tasks-v1";
      var SETTINGS_KEY = "pomodoro-kanban-settings-v1";

      var STATUS_META = {
        todo: { label: "待办", listId: "todoList", countId: "todoCount", empty: "暂无待办任务。新增一个任务后,它会出现在这里。" },
        doing: { label: "进行中", listId: "doingList", countId: "doingCount", empty: "暂无进行中的任务。可将待办卡片拖拽到这里。" },
        done: { label: "已完成", listId: "doneList", countId: "doneCount", empty: "暂无已完成任务。完成后拖拽到这里归档。" }
      };

      var PRIORITY_META = {
        high: { label: "高", className: "high" },
        medium: { label: "中", className: "medium" },
        low: { label: "低", className: "low" }
      };

      var tasks = [];
      var timers = {};

      var taskForm = document.getElementById("taskForm");
      var taskTitle = document.getElementById("taskTitle");
      var taskPomodoros = document.getElementById("taskPomodoros");
      var taskPriority = document.getElementById("taskPriority");
      var searchInput = document.getElementById("searchInput");
      var demoToggle = document.getElementById("demoToggle");
      var formMessage = document.getElementById("formMessage");
      var appMessage = document.getElementById("appMessage");
      var clearDoneBtn = document.getElementById("clearDoneBtn");

      function defaultDuration() {
        return demoToggle.checked ? 25 : 25 * 60;
      }

      function formatTime(seconds) {
        seconds = Math.max(0, Number(seconds) || 0);
        var m = Math.floor(seconds / 60);
        var s = seconds % 60;
        return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
      }

      function showMessage(el, text, type) {
        el.textContent = text || "";
        el.className = "message" + (el === appMessage ? " app-message" : "");
        if (text) {
          el.classList.add("show", type || "info");
        }
      }

      function saveTasks() {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
        } catch (err) {
          showMessage(appMessage, "无法保存到 localStorage:浏览器存储可能已满或被禁用。", "error");
        }
      }

      function saveSettings() {
        try {
          localStorage.setItem(SETTINGS_KEY, JSON.stringify({ demo: demoToggle.checked }));
        } catch (err) {
          showMessage(appMessage, "设置保存失败,但不影响当前页面使用。", "error");
        }
      }

      function loadSettings() {
        try {
          var raw = localStorage.getItem(SETTINGS_KEY);
          if (!raw) return;
          var data = JSON.parse(raw);
          demoToggle.checked = Boolean(data.demo);
        } catch (err) {
          demoToggle.checked = false;
        }
      }

      function normalizeTask(task) {
        if (!task || typeof task !== "object") return null;
        var title = String(task.title || "").trim();
        var estimate = parseInt(task.estimate, 10);
        var priority = PRIORITY_META[task.priority] ? task.priority : "medium";
        var status = STATUS_META[task.status] ? task.status : "todo";
        if (!title) return null;
        if (!Number.isFinite(estimate) || estimate < 1) estimate = 1;
        if (estimate > 20) estimate = 20;
        return {
          id: String(task.id || makeId()),
          title: title.slice(0, 80),
          estimate: estimate,
          priority: priority,
          status: status,
          createdAt: Number(task.createdAt) || Date.now()
        };
      }

      function loadTasks() {
        try {
          var raw = localStorage.getItem(STORAGE_KEY);
          if (!raw) {
            tasks = [];
            return;
          }
          var parsed = JSON.parse(raw);
          if (!Array.isArray(parsed)) {
            tasks = [];
            showMessage(appMessage, "已忽略损坏的任务数据。", "error");
            return;
          }
          tasks = parsed.map(normalizeTask).filter(Boolean);
        } catch (err) {
          tasks = [];
          showMessage(appMessage, "读取任务数据失败,已使用空看板。", "error");
        }
      }

      function makeId() {
        return "t_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 8);
      }

      function priorityLabel(priority) {
        return PRIORITY_META[priority] ? PRIORITY_META[priority].label : PRIORITY_META.medium.label;
      }

      function validateForm() {
        var title = taskTitle.value.trim();
        var estimate = parseInt(taskPomodoros.value, 10);
        var priority = taskPriority.value;

        if (!title) {
          return { ok: false, message: "请输入任务标题。" };
        }

        if (title.length > 80) {
          return { ok: false, message: "任务标题不能超过 80 个字符。" };
        }

        if (!Number.isFinite(estimate) || estimate < 1 || estimate > 20) {
          return { ok: false, message: "预估番茄数必须是 1 到 20 之间的整数。" };
        }

        if (!PRIORITY_META[priority]) {
          return { ok: false, message: "请选择有效的优先级。" };
        }

        return {
          ok: true,
          value: {
            id: makeId(),
            title: title,
            estimate: estimate,
            priority: priority,
            status: "todo",
            createdAt: Date.now()
          }
        };
      }

      function sortTasks(a, b) {
        var weight = { high: 0, medium: 1, low: 2 };
        var aw = weight[a.priority] == null ? 1 : weight[a.priority];
        var bw = weight[b.priority] == null ? 1 : weight[b.priority];
        if (aw !== bw) return aw - bw;
        return a.createdAt - b.createdAt;
      }

      function render() {
        var query = searchInput.value.trim().toLowerCase();

        Object.keys(STATUS_META).forEach(function (status) {
          var meta = STATUS_META[status];
          var list = document.getElementById(meta.listId);
          var count = document.getElementById(meta.countId);
          var statusTasks = tasks.filter(function (task) {
            return task.status === status;
          }).sort(sortTasks);
          var visibleTasks = statusTasks.filter(function (task) {
            return !query || task.title.toLowerCase().indexOf(query) !== -1;
          });

          count.textContent = String(statusTasks.length);
          list.innerHTML = "";

          if (visibleTasks.length === 0) {
            var empty = document.createElement("div");
            empty.className = "empty";
            empty.textContent = query && statusTasks.length > 0
              ? "当前列表没有匹配搜索条件的任务。"
              : meta.empty;
            list.appendChild(empty);
          } else {
            visibleTasks.forEach(function (task) {
              list.appendChild(createCard(task));
              updateTimerUI(task.id);
            });
          }
        });

        var doneCount = tasks.filter(function (task) { return task.status === "done"; }).length;
        clearDoneBtn.disabled = doneCount === 0;
      }

      function createCard(task) {
        var card = document.createElement("article");
        card.className = "card priority-" + task.priority;
        card.draggable = true;
        card.dataset.id = task.id;

        card.addEventListener("dragstart", function (event) {
          event.dataTransfer.setData("text/plain", task.id);
          event.dataTransfer.effectAllowed = "move";
          card.classList.add("dragging");
        });

        card.addEventListener("dragend", function () {
          card.classList.remove("dragging");
          document.querySelectorAll(".column").forEach(function (col) {
            col.classList.remove("drag-over");
          });
        });

        var top = document.createElement("div");
        top.className = "card-top";

        var title = document.createElement("h3");
        title.textContent = task.title;

        var pill = document.createElement("span");
        pill.className = "pill " + (PRIORITY_META[task.priority] ? PRIORITY_META[task.priority].className : "medium");
        pill.textContent = "优先级 " + priorityLabel(task.priority);

        top.appendChild(title);
        top.appendChild(pill);

        var meta = document.createElement("div");
        meta.className = "meta";

        var estimate = document.createElement("span");
        estimate.textContent = "预估 " + task.estimate + " 个番茄";

        var status = document.createElement("span");
        status.textContent = "状态:" + STATUS_META[task.status].label;

        meta.appendChild(estimate);
        meta.appendChild(status);

        var timer = document.createElement("div");
        timer.className = "timer";

        var timerRow = document.createElement("div");
        timerRow.className = "timer-row";

        var time = document.createElement("div");
        time.className = "timer-time";
        time.dataset.timerTime = task.id;
        time.textContent = formatTime(defaultDuration());

        var state = document.createElement("div");
        state.className = "timer-state";
        state.dataset.timerState = task.id;
        state.textContent = "尚未开始";

        timerRow.appendChild(time);
        timerRow.appendChild(state);

        var actions = document.createElement("div");
        actions.className = "timer-actions";

        var startBtn = document.createElement("button");
        startBtn.type = "button";
        startBtn.className = "secondary-btn";
        startBtn.dataset.timerStart = task.id;
        startBtn.textContent = "开始 25 分钟番茄钟";
        startBtn.addEventListener("click", function () {
          startTimer(task.id);
        });

        var pauseBtn = document.createElement("button");
        pauseBtn.type = "button";
        pauseBtn.className = "ghost-btn";
        pauseBtn.dataset.timerPause = task.id;
        pauseBtn.textContent = "暂停";
        pauseBtn.addEventListener("click", function () {
          pauseTimer(task.id);
        });

        var resetBtn = document.createElement("button");
        resetBtn.type = "button";
        resetBtn.className = "ghost-btn";
        resetBtn.dataset.timerReset = task.id;
        resetBtn.textContent = "重置";
        resetBtn.addEventListener("click", function () {
          resetTimer(task.id);
        });

        actions.appendChild(startBtn);
        actions.appendChild(pauseBtn);
        actions.appendChild(resetBtn);

        timer.appendChild(timerRow);
        timer.appendChild(actions);

        var footer = document.createElement("div");
        footer.className = "card-footer";

        var hint = document.createElement("span");
        hint.className = "drag-hint";
        hint.textContent = "可拖拽到其他列表";

        var selectLabel = document.createElement("label");
        selectLabel.className = "sr-only";
        selectLabel.setAttribute("for", "move_" + task.id);
        selectLabel.textContent = "移动任务状态";

        var moveSelect = document.createElement("select");
        moveSelect.className = "move-select";
        moveSelect.id = "move_" + task.id;
        moveSelect.setAttribute("aria-label", "移动任务状态");
        Object.keys(STATUS_META).forEach(function (key) {
          var option = document.createElement("option");
          option.value = key;
          option.textContent = STATUS_META[key].label;
          option.selected = key === task.status;
          moveSelect.appendChild(option);
        });
        moveSelect.addEventListener("change", function () {
          moveTask(task.id, moveSelect.value);
        });

        footer.appendChild(hint);
        footer.appendChild(selectLabel);
        footer.appendChild(moveSelect);

        card.appendChild(top);
        card.appendChild(meta);
        card.appendChild(timer);
        card.appendChild(footer);

        return card;
      }

      function moveTask(id, nextStatus) {
        if (!STATUS_META[nextStatus]) return;
        var task = tasks.find(function (item) {
          return item.id === id;
        });
        if (!task) return;
        if (task.status === nextStatus) return;
        task.status = nextStatus;
        saveTasks();
        render();
        showMessage(appMessage, "任务已移动到「" + STATUS_META[nextStatus].label + "」。", "success");
      }

      function ensureTimer(id) {
        if (!timers[id]) {
          var duration = defaultDuration();
          timers[id] = {
            duration: duration,
            remaining: duration,
            running: false,
            done: false
          };
        }
        return timers[id];
      }

      function startTimer(id) {
        var timer = ensureTimer(id);
        if (timer.remaining <= 0 || timer.done) {
          timer.duration = defaultDuration();
          timer.remaining = timer.duration;
          timer.done = false;
        }
        timer.running = true;
        updateTimerUI(id);
      }

      function pauseTimer(id) {
        var timer = timers[id];
        if (!timer) return;
        timer.running = false;
        updateTimerUI(id);
      }

      function resetTimer(id) {
        var duration = defaultDuration();
        timers[id] = {
          duration: duration,
          remaining: duration,
          running: false,
          done: false
        };
        updateTimerUI(id);
      }

      function updateTimerUI(id) {
        var timer = timers[id];
        var remaining = timer ? timer.remaining : defaultDuration();
        var timeEl = document.querySelector('[data-timer-time="' + id + '"]');
        var stateEl = document.querySelector('[data-timer-state="' + id + '"]');
        var startBtn = document.querySelector('[data-timer-start="' + id + '"]');
        var pauseBtn = document.querySelector('[data-timer-pause="' + id + '"]');
        var resetBtn = document.querySelector('[data-timer-reset="' + id + '"]');

        if (!timeEl || !stateEl || !startBtn || !pauseBtn || !resetBtn) return;

        timeEl.textContent = formatTime(remaining);

        if (!timer) {
          stateEl.textContent = demoToggle.checked ? "尚未开始 · 演示 25 秒" : "尚未开始";
          startBtn.textContent = "开始 25 分钟番茄钟";
          startBtn.disabled = false;
          pauseBtn.disabled = true;
          resetBtn.disabled = true;
          return;
        }

        if (timer.running) {
          stateEl.textContent = "专注中";
          startBtn.textContent = "继续";
          startBtn.disabled = true;
          pauseBtn.disabled = false;
          resetBtn.disabled = false;
        } else if (timer.done || timer.remaining <= 0) {
          stateEl.textContent = "本轮完成";
          startBtn.textContent = "开始 25 分钟番茄钟";
          startBtn.disabled = false;
          pauseBtn.disabled = true;
          resetBtn.disabled = false;
        } else if (timer.remaining < timer.duration) {
          stateEl.textContent = "已暂停";
          startBtn.textContent = "继续";
          startBtn.disabled = false;
          pauseBtn.disabled = true;
          resetBtn.disabled = false;
        } else {
          stateEl.textContent = demoToggle.checked ? "尚未开始 · 演示 25 秒" : "尚未开始";
          startBtn.textContent = "开始 25 分钟番茄钟";
          startBtn.disabled = false;
          pauseBtn.disabled = true;
          resetBtn.disabled = false;
        }
      }

      function tickTimers() {
        Object.keys(timers).forEach(function (id) {
          var timer = timers[id];
          if (!timer.running) return;

          timer.remaining -= 1;
          if (timer.remaining <= 0) {
            timer.remaining = 0;
            timer.running = false;
            timer.done = true;
            showMessage(appMessage, "一个番茄钟已完成。可以重置后开始下一轮。", "success");
          }
          updateTimerUI(id);
        });
      }

      taskForm.addEventListener("submit", function (event) {
        event.preventDefault();

        var result = validateForm();
        if (!result.ok) {
          showMessage(formMessage, result.message, "error");
          return;
        }

        tasks.push(result.value);
        saveTasks();
        render();

        taskForm.reset();
        taskPomodoros.value = "1";
        taskPriority.value = "medium";
        taskTitle.focus();
        showMessage(formMessage, "任务已新增到「待办」。", "success");
      });

      searchInput.addEventListener("input", function () {
        render();
      });

      demoToggle.addEventListener("change", function () {
        saveSettings();
        Object.keys(timers).forEach(function (id) {
          var timer = timers[id];
          if (!timer.running && (timer.remaining === timer.duration || timer.remaining <= 0 || timer.done)) {
            timer.duration = defaultDuration();
            timer.remaining = timer.duration;
            timer.done = false;
          }
        });
        render();
        showMessage(appMessage, demoToggle.checked ? "演示加速已开启:25 分钟将按 25 秒运行。" : "演示加速已关闭:番茄钟恢复为 25 分钟。", "info");
      });

      clearDoneBtn.addEventListener("click", function () {
        var removedIds = tasks.filter(function (task) {
          return task.status === "done";
        }).map(function (task) {
          return task.id;
        });

        if (removedIds.length === 0) {
          showMessage(appMessage, "当前没有已完成任务可清空。", "info");
          return;
        }

        tasks = tasks.filter(function (task) {
          return task.status !== "done";
        });

        removedIds.forEach(function (id) {
          delete timers[id];
        });

        saveTasks();
        render();
        showMessage(appMessage, "已清空 " + removedIds.length + " 个已完成任务。", "success");
      });

      document.querySelectorAll(".column").forEach(function (column) {
        column.addEventListener("dragover", function (event) {
          event.preventDefault();
          event.dataTransfer.dropEffect = "move";
          column.classList.add("drag-over");
        });

        column.addEventListener("dragleave", function (event) {
          if (!column.contains(event.relatedTarget)) {
            column.classList.remove("drag-over");
          }
        });

        column.addEventListener("drop", function (event) {
          event.preventDefault();
          column.classList.remove("drag-over");
          var id = event.dataTransfer.getData("text/plain");
          var nextStatus = column.dataset.status;
          moveTask(id, nextStatus);
        });
      });

      loadSettings();
      loadTasks();
      render();
      setInterval(tickTimers, 1000);
    })();
  </script>
</body>
</html>
有真实输出 完整 HTML 包含交互脚本 无外部资源 无 Markdown 包裹

PackyAPI

packyApiClaudeOpus4.7 · claude-opus-4-7

Succeeded

2026-05-23 17:04:54 · 97079 ms · 输入 659 / 输出 9,361 / 总计 10,020 tokens

查看完整源码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>番茄任务看板</title>
<style>
  *, *::before, *::after { box-sizing: border-box; }
  :root {
    --bg: #f5f6f8;
    --surface: #ffffff;
    --border: #e3e5ea;
    --text: #1f2330;
    --muted: #6b7280;
    --primary: #c0392b;
    --primary-hover: #a93222;
    --accent: #2c3e50;
    --success: #2f855a;
    --warning: #b45309;
    --danger: #b91c1c;
    --shadow: 0 1px 2px rgba(0,0,0,.04), 0 2px 8px rgba(0,0,0,.04);
  }
  html, body { margin: 0; padding: 0; }
  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
    background: var(--bg);
    color: var(--text);
    font-size: 14px;
    line-height: 1.5;
    min-height: 100vh;
  }
  header.app-header {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: 14px 20px;
    position: sticky;
    top: 0;
    z-index: 10;
  }
  .header-row {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
    justify-content: space-between;
    max-width: 1400px;
    margin: 0 auto;
  }
  h1 {
    font-size: 16px;
    margin: 0;
    font-weight: 600;
    letter-spacing: .3px;
  }
  h1 .dot {
    display: inline-block;
    width: 8px; height: 8px;
    background: var(--primary);
    border-radius: 50%;
    margin-right: 8px;
    vertical-align: middle;
  }
  .controls {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
  }
  input[type="text"], input[type="number"], select, input[type="search"] {
    font: inherit;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 7px 10px;
    outline: none;
    transition: border-color .15s, box-shadow .15s;
  }
  input:focus, select:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(44,62,80,.12);
  }
  button {
    font: inherit;
    cursor: pointer;
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text);
    border-radius: 6px;
    padding: 7px 12px;
    transition: background .15s, border-color .15s, color .15s;
  }
  button:hover { background: #f0f1f4; }
  button.primary {
    background: var(--primary);
    color: #fff;
    border-color: var(--primary);
  }
  button.primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
  button.ghost { background: transparent; }
  button.danger { color: var(--danger); }
  button.danger:hover { background: #fdecec; }
  .switch {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    user-select: none;
    cursor: pointer;
    font-size: 13px;
    color: var(--muted);
  }
  .switch input { display: none; }
  .switch .track {
    width: 34px; height: 18px;
    background: #cbd0d8;
    border-radius: 999px;
    position: relative;
    transition: background .15s;
  }
  .switch .track::after {
    content: "";
    position: absolute;
    top: 2px; left: 2px;
    width: 14px; height: 14px;
    background: #fff;
    border-radius: 50%;
    transition: transform .15s;
    box-shadow: 0 1px 2px rgba(0,0,0,.2);
  }
  .switch input:checked + .track { background: var(--primary); }
  .switch input:checked + .track::after { transform: translateX(16px); }

  main {
    max-width: 1400px;
    margin: 0 auto;
    padding: 16px 20px 40px;
  }
  .new-task {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 14px;
    margin-bottom: 16px;
    box-shadow: var(--shadow);
  }
  .new-task form {
    display: grid;
    grid-template-columns: 2fr 1fr 1fr auto;
    gap: 8px;
    align-items: start;
  }
  .field { display: flex; flex-direction: column; gap: 4px; }
  .field label {
    font-size: 12px;
    color: var(--muted);
  }
  .form-error {
    color: var(--danger);
    font-size: 12px;
    margin-top: 8px;
    min-height: 16px;
  }
  .board {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 14px;
  }
  .column {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    min-height: 320px;
    box-shadow: var(--shadow);
  }
  .column-head {
    padding: 12px 14px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-weight: 600;
    font-size: 13px;
    letter-spacing: .3px;
  }
  .column-head .count {
    font-weight: 400;
    color: var(--muted);
    font-size: 12px;
    background: #f0f1f4;
    border-radius: 999px;
    padding: 2px 8px;
  }
  .column-body {
    flex: 1;
    padding: 10px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: 200px;
  }
  .column-body.drag-over {
    background: #fffaf0;
    outline: 2px dashed var(--primary);
    outline-offset: -6px;
    border-radius: 8px;
  }
  .empty-state {
    text-align: center;
    color: var(--muted);
    padding: 24px 10px;
    font-size: 13px;
    border: 1px dashed var(--border);
    border-radius: 6px;
  }
  .card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: 6px;
    padding: 10px 12px;
    cursor: grab;
    display: flex;
    flex-direction: column;
    gap: 8px;
    transition: box-shadow .15s, transform .15s;
  }
  .card:hover { box-shadow: 0 2px 6px rgba(0,0,0,.06); }
  .card.dragging { opacity: .5; }
  .card.priority-high { border-left-color: var(--danger); }
  .card.priority-medium { border-left-color: var(--warning); }
  .card.priority-low { border-left-color: var(--success); }
  .card-title {
    font-weight: 600;
    font-size: 14px;
    word-break: break-word;
  }
  .card-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    font-size: 11px;
  }
  .tag {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    background: #f0f1f4;
    color: var(--muted);
  }
  .tag.high { background: #fdecec; color: var(--danger); }
  .tag.medium { background: #fff4e0; color: var(--warning); }
  .tag.low { background: #e6f4ec; color: var(--success); }
  .card-actions {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    align-items: center;
  }
  .card-actions button {
    font-size: 12px;
    padding: 4px 8px;
  }
  .timer {
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    color: var(--primary);
    font-weight: 600;
    margin-left: auto;
  }
  .timer.done { color: var(--success); }
  .pomo-progress {
    height: 4px;
    background: #f0f1f4;
    border-radius: 999px;
    overflow: hidden;
  }
  .pomo-progress > div {
    height: 100%;
    background: var(--primary);
    width: 0%;
    transition: width .3s linear;
  }
  .search-row {
    display: flex;
    gap: 8px;
    align-items: center;
  }
  .search-row input[type="search"] { width: 220px; }

  @media (max-width: 900px) {
    .board { grid-template-columns: 1fr; }
    .new-task form { grid-template-columns: 1fr 1fr; }
    .new-task form .field-title { grid-column: 1 / -1; }
    .new-task form .submit-wrap { grid-column: 1 / -1; }
  }
  @media (max-width: 480px) {
    header.app-header { padding: 12px 14px; }
    main { padding: 12px 14px 32px; }
    .search-row input[type="search"] { width: 100%; }
    .controls { width: 100%; }
    .search-row { flex: 1; }
    .new-task form { grid-template-columns: 1fr; }
  }
</style>
</head>
<body>
<header class="app-header">
  <div class="header-row">
    <h1><span class="dot"></span>番茄任务看板</h1>
    <div class="controls">
      <div class="search-row">
        <input type="search" id="search" placeholder="搜索任务标题…" aria-label="搜索任务">
      </div>
      <label class="switch" title="开启后 25 分钟将以 25 秒运行,便于演示">
        <input type="checkbox" id="demoMode">
        <span class="track"></span>
        <span>演示加速</span>
      </label>
      <button id="clearDone" class="ghost danger" type="button">清空已完成</button>
    </div>
  </div>
</header>

<main>
  <section class="new-task" aria-label="新建任务">
    <form id="taskForm" novalidate>
      <div class="field field-title">
        <label for="title">任务标题</label>
        <input type="text" id="title" maxlength="80" placeholder="例如:实现用户登录接口" required>
      </div>
      <div class="field">
        <label for="estimate">预估番茄数</label>
        <input type="number" id="estimate" min="1" max="20" value="1" required>
      </div>
      <div class="field">
        <label for="priority">优先级</label>
        <select id="priority">
          <option value="high">高</option>
          <option value="medium" selected>中</option>
          <option value="low">低</option>
        </select>
      </div>
      <div class="field submit-wrap">
        <label>&nbsp;</label>
        <button type="submit" class="primary">添加任务</button>
      </div>
      <div class="form-error" id="formError" role="alert"></div>
    </form>
  </section>

  <section class="board" aria-label="任务看板">
    <div class="column" data-status="todo">
      <div class="column-head"><span>待办</span><span class="count" id="count-todo">0</span></div>
      <div class="column-body" data-status="todo" id="col-todo"></div>
    </div>
    <div class="column" data-status="doing">
      <div class="column-head"><span>进行中</span><span class="count" id="count-doing">0</span></div>
      <div class="column-body" data-status="doing" id="col-doing"></div>
    </div>
    <div class="column" data-status="done">
      <div class="column-head"><span>已完成</span><span class="count" id="count-done">0</span></div>
      <div class="column-body" data-status="done" id="col-done"></div>
    </div>
  </section>
</main>

<script>
(function(){
  "use strict";

  const STORAGE_KEY = "pomo-board-v1";
  const DEMO_KEY = "pomo-board-demo";
  const FULL_SECONDS = 25 * 60;
  const DEMO_SECONDS = 25;

  /** @type {{id:string,title:string,estimate:number,priority:string,status:string,completedPomos:number,timer:{remaining:number,running:boolean,total:number}|null,createdAt:number}[]} */
  let tasks = [];
  let demoMode = false;
  let searchQuery = "";
  const tickHandles = {}; // taskId -> intervalId

  // ---------- Persistence ----------
  function load() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (raw) {
        const arr = JSON.parse(raw);
        if (Array.isArray(arr)) tasks = arr;
      }
    } catch (e) { tasks = []; }
    try {
      demoMode = localStorage.getItem(DEMO_KEY) === "1";
    } catch(e){}
    // make sure no timer auto-resumes after refresh
    tasks.forEach(t => { if (t.timer) t.timer.running = false; });
  }
  function save() {
    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks)); } catch(e){}
  }
  function saveDemo() {
    try { localStorage.setItem(DEMO_KEY, demoMode ? "1" : "0"); } catch(e){}
  }

  // ---------- Helpers ----------
  function uid() {
    return "t_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 7);
  }
  function fmt(sec) {
    sec = Math.max(0, Math.floor(sec));
    const m = Math.floor(sec / 60);
    const s = sec % 60;
    return (m < 10 ? "0"+m : m) + ":" + (s < 10 ? "0"+s : s);
  }
  function totalForTimer() {
    return demoMode ? DEMO_SECONDS : FULL_SECONDS;
  }
  function priorityLabel(p) {
    return p === "high" ? "高优先级" : p === "low" ? "低优先级" : "中优先级";
  }
  function escapeHtml(s) {
    return String(s).replace(/[&<>"']/g, c => ({
      "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
    }[c]));
  }

  // ---------- Rendering ----------
  function render() {
    const cols = {
      todo: document.getElementById("col-todo"),
      doing: document.getElementById("col-doing"),
      done: document.getElementById("col-done"),
    };
    Object.values(cols).forEach(c => c.innerHTML = "");

    const q = searchQuery.trim().toLowerCase();
    const counts = { todo:0, doing:0, done:0 };
    const filtered = tasks.filter(t => !q || t.title.toLowerCase().includes(q));

    // counts reflect filtered visible
    filtered.forEach(t => counts[t.status] = (counts[t.status]||0)+1);

    for (const status of ["todo","doing","done"]) {
      const list = filtered.filter(t => t.status === status);
      if (list.length === 0) {
        const empty = document.createElement("div");
        empty.className = "empty-state";
        empty.textContent = q
          ? "没有匹配的任务"
          : status === "todo" ? "暂无待办任务,先添加一个吧"
          : status === "doing" ? "把卡片拖到这里开始专注"
          : "完成的任务会显示在这里";
        cols[status].appendChild(empty);
      } else {
        list.forEach(t => cols[status].appendChild(renderCard(t)));
      }
      document.getElementById("count-"+status).textContent = counts[status];
    }
  }

  function renderCard(task) {
    const el = document.createElement("div");
    el.className = "card priority-" + task.priority;
    el.draggable = true;
    el.dataset.id = task.id;

    const title = document.createElement("div");
    title.className = "card-title";
    title.textContent = task.title;

    const meta = document.createElement("div");
    meta.className = "card-meta";
    meta.innerHTML =
      '<span class="tag '+task.priority+'">'+priorityLabel(task.priority)+'</span>' +
      '<span class="tag">???? '+task.completedPomos+' / '+task.estimate+'</span>';

    const actions = document.createElement("div");
    actions.className = "card-actions";

    const hasTimer = !!task.timer;
    const running = hasTimer && task.timer.running;

    if (!hasTimer) {
      const startBtn = document.createElement("button");
      startBtn.type = "button";
      startBtn.className = "primary";
      startBtn.textContent = "开始 25 分钟";
      startBtn.addEventListener("click", () => startPomo(task.id));
      actions.appendChild(startBtn);
    } else {
      const toggleBtn = document.createElement("button");
      toggleBtn.type = "button";
      toggleBtn.className = running ? "" : "primary";
      toggleBtn.textContent = running ? "暂停" : "继续";
      toggleBtn.addEventListener("click", () => togglePomo(task.id));
      actions.appendChild(toggleBtn);

      const resetBtn = document.createElement("button");
      resetBtn.type = "button";
      resetBtn.textContent = "重置";
      resetBtn.addEventListener("click", () => resetPomo(task.id));
      actions.appendChild(resetBtn);
    }

    const delBtn = document.createElement("button");
    delBtn.type = "button";
    delBtn.className = "ghost danger";
    delBtn.textContent = "删除";
    delBtn.addEventListener("click", () => deleteTask(task.id));
    actions.appendChild(delBtn);

    const timerEl = document.createElement("span");
    timerEl.className = "timer";
    if (hasTimer) {
      timerEl.textContent = fmt(task.timer.remaining);
      if (task.timer.remaining <= 0) timerEl.classList.add("done");
    } else {
      timerEl.textContent = "";
    }
    actions.appendChild(timerEl);

    el.appendChild(title);
    el.appendChild(meta);
    el.appendChild(actions);

    if (hasTimer) {
      const prog = document.createElement("div");
      prog.className = "pomo-progress";
      const inner = document.createElement("div");
      const total = task.timer.total || totalForTimer();
      const pct = Math.max(0, Math.min(100, ((total - task.timer.remaining) / total) * 100));
      inner.style.width = pct + "%";
      prog.appendChild(inner);
      el.appendChild(prog);
    }

    // Drag events
    el.addEventListener("dragstart", e => {
      el.classList.add("dragging");
      e.dataTransfer.effectAllowed = "move";
      e.dataTransfer.setData("text/plain", task.id);
    });
    el.addEventListener("dragend", () => {
      el.classList.remove("dragging");
    });

    return el;
  }

  // ---------- Pomodoro logic ----------
  function startPomo(id) {
    const t = tasks.find(x => x.id === id);
    if (!t) return;
    if (t.status === "done") return;
    if (t.status === "todo") t.status = "doing";
    const total = totalForTimer();
    t.timer = { remaining: total, running: true, total };
    save();
    render();
    startTick(id);
  }
  function togglePomo(id) {
    const t = tasks.find(x => x.id === id);
    if (!t || !t.timer) return;
    if (t.timer.remaining <= 0) return;
    t.timer.running = !t.timer.running;
    save();
    render();
    if (t.timer.running) startTick(id);
    else stopTick(id);
  }
  function resetPomo(id) {
    const t = tasks.find(x => x.id === id);
    if (!t || !t.timer) return;
    stopTick(id);
    const total = totalForTimer();
    t.timer = { remaining: total, running: false, total };
    save();
    render();
  }
  function startTick(id) {
    stopTick(id);
    tickHandles[id] = setInterval(() => {
      const t = tasks.find(x => x.id === id);
      if (!t || !t.timer || !t.timer.running) { stopTick(id); return; }
      t.timer.remaining -= 1;
      if (t.timer.remaining <= 0) {
        t.timer.remaining = 0;
        t.timer.running = false;
        t.completedPomos = (t.completedPomos || 0) + 1;
        stopTick(id);
        save();
        render();
        // light notification
        try {
          // simple beep using Web Audio API (no external resource)
          const ctx = new (window.AudioContext || window.webkitAudioContext)();
          const o = ctx.createOscillator();
          const g = ctx.createGain();
          o.connect(g); g.connect(ctx.destination);
          o.frequency.value = 880;
          g.gain.value = 0.05;
          o.start();
          setTimeout(()=>{ o.stop(); ctx.close(); }, 250);
        } catch(e){}
        return;
      }
      save();
      // update only timer text & progress for performance
      const card = document.querySelector('.card[data-id="'+id+'"]');
      if (card) {
        const timerEl = card.querySelector(".timer");
        if (timerEl) timerEl.textContent = fmt(t.timer.remaining);
        const prog = card.querySelector(".pomo-progress > div");
        if (prog) {
          const total = t.timer.total || totalForTimer();
          prog.style.width = (((total - t.timer.remaining)/total)*100) + "%";
        }
      }
    }, 1000);
  }
  function stopTick(id) {
    if (tickHandles[id]) {
      clearInterval(tickHandles[id]);
      delete tickHandles[id];
    }
  }
  function stopAllTicks() {
    Object.keys(tickHandles).forEach(stopTick);
  }

  // ---------- CRUD ----------
  function addTask(data) {
    tasks.unshift({
      id: uid(),
      title: data.title,
      estimate: data.estimate,
      priority: data.priority,
      status: "todo",
      completedPomos: 0,
      timer: null,
      createdAt: Date.now(),
    });
    save();
    render();
  }
  function deleteTask(id) {
    stopTick(id);
    tasks = tasks.filter(t => t.id !== id);
    save();
    render();
  }
  function moveTask(id, newStatus) {
    const t = tasks.find(x => x.id === id);
    if (!t || t.status === newStatus) return;
    t.status = newStatus;
    if (newStatus === "done") {
      // stop any running timer
      if (t.timer && t.timer.running) {
        t.timer.running = false;
        stopTick(id);
      }
    }
    save();
    render();
  }
  function clearDone() {
    const doneTasks = tasks.filter(t => t.status === "done");
    if (doneTasks.length === 0) {
      alert("当前没有已完成的任务");
      return;
    }
    if (!confirm("确定要清空 " + doneTasks.length + " 个已完成的任务吗?")) return;
    doneTasks.forEach(t => stopTick(t.id));
    tasks = tasks.filter(t => t.status !== "done");
    save();
    render();
  }

  // ---------- Form ----------
  const form = document.getElementById("taskForm");
  const errorEl = document.getElementById("formError");
  form.addEventListener("submit", function(e){
    e.preventDefault();
    errorEl.textContent = "";
    const titleInput = document.getElementById("title");
    const estimateInput = document.getElementById("estimate");
    const priorityInput = document.getElementById("priority");

    const title = (titleInput.value || "").trim();
    const estimate = parseInt(estimateInput.value, 10);
    const priority = priorityInput.value;

    if (!title) { errorEl.textContent = "请输入任务标题"; titleInput.focus(); return; }
    if (title.length > 80) { errorEl.textContent = "标题不能超过 80 个字符"; return; }
    if (!estimate || isNaN(estimate) || estimate < 1 || estimate > 20) {
      errorEl.textContent = "预估番茄数应在 1 - 20 之间"; estimateInput.focus(); return;
    }
    if (!["high","medium","low"].includes(priority)) {
      errorEl.textContent = "请选择优先级"; return;
    }

    addTask({ title, estimate, priority });
    form.reset();
    document.getElementById("estimate").value = 1;
    document.getElementById("priority").value = "medium";
    titleInput.focus();
  });

  // ---------- DnD ----------
  document.querySelectorAll(".column-body").forEach(col => {
    col.addEventListener("dragover", e => {
      e.preventDefault();
      e.dataTransfer.dropEffect = "move";
      col.classList.add("drag-over");
    });
    col.addEventListener("dragleave", () => col.classList.remove("drag-over"));
    col.addEventListener("drop", e => {
      e.preventDefault();
      col.classList.remove("drag-over");
      const id = e.dataTransfer.getData("text/plain");
      const status = col.dataset.status;
      if (id && status) moveTask(id, status);
    });
  });

  // ---------- Search ----------
  document.getElementById("search").addEventListener("input", function(e){
    searchQuery = e.target.value || "";
    render();
  });

  // ---------- Demo mode ----------
  const demoToggle = document.getElementById("demoMode");
  demoToggle.addEventListener("change", function(){
    demoMode = demoToggle.checked;
    saveDemo();
    // Don't retroactively change existing timers; new timers will use new value.
  });

  // ---------- Clear done ----------
  document.getElementById("clearDone").addEventListener("click", clearDone);

  // ---------- Init ----------
  load();
  demoToggle.checked = demoMode;
  render();

  window.addEventListener("beforeunload", () => {
    // pause all timers so they don't auto-resume
    tasks.forEach(t => { if (t.timer) t.timer.running = false; });
    save();
    stopAllTicks();
  });
})();
</script>
</body>
</html>
有真实输出 完整 HTML 包含交互脚本 无外部资源 无 Markdown 包裹

OfoxAI

ofoxClaudeOpus4.7 · anthropic/claude-opus-4.7

Succeeded

2026-05-23 16:49:51 · 50186 ms · 输入 602 / 输出 8,472 / 总计 9,074 tokens

查看完整源码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>番茄任务看板</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #f5f6f8;
    --panel: #ffffff;
    --border: #e3e6eb;
    --text: #1f2937;
    --muted: #6b7280;
    --accent: #c0392b;
    --accent-hover: #a93226;
    --shadow: 0 1px 2px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.06);
    --p-high: #dc2626;
    --p-mid: #d97706;
    --p-low: #2563eb;
  }
  html, body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
    background: var(--bg);
    color: var(--text);
    font-size: 14px;
    line-height: 1.5;
    min-height: 100vh;
  }
  header {
    background: var(--panel);
    border-bottom: 1px solid var(--border);
    padding: 14px 20px;
    position: sticky;
    top: 0;
    z-index: 10;
  }
  .header-inner {
    max-width: 1280px;
    margin: 0 auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px;
  }
  h1 {
    font-size: 16px;
    font-weight: 600;
    letter-spacing: 0.5px;
    flex-shrink: 0;
  }
  .toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin-left: auto;
  }
  input[type="text"], input[type="number"], select, textarea {
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 7px 10px;
    font-size: 13px;
    background: #fff;
    color: var(--text);
    font-family: inherit;
    outline: none;
    transition: border-color 0.15s;
  }
  input[type="text"]:focus, input[type="number"]:focus, select:focus {
    border-color: var(--accent);
  }
  .search {
    width: 200px;
  }
  button {
    border: 1px solid var(--border);
    background: #fff;
    color: var(--text);
    padding: 7px 12px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 13px;
    font-family: inherit;
    transition: background 0.15s, border-color 0.15s;
  }
  button:hover { background: #f3f4f6; }
  button.primary {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
  }
  button.primary:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
  }
  button.danger {
    color: #dc2626;
  }
  button.danger:hover {
    background: #fef2f2;
    border-color: #fecaca;
  }
  .switch {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: var(--muted);
    cursor: pointer;
    user-select: none;
  }
  .switch input {
    appearance: none;
    width: 32px;
    height: 18px;
    background: #d1d5db;
    border-radius: 999px;
    position: relative;
    cursor: pointer;
    transition: background 0.15s;
  }
  .switch input:checked { background: var(--accent); }
  .switch input::after {
    content: "";
    position: absolute;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: #fff;
    top: 2px;
    left: 2px;
    transition: left 0.15s;
  }
  .switch input:checked::after { left: 16px; }

  main {
    max-width: 1280px;
    margin: 0 auto;
    padding: 20px;
  }

  .add-form {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 14px;
    margin-bottom: 20px;
    box-shadow: var(--shadow);
    display: grid;
    grid-template-columns: 2fr 1fr 1fr auto;
    gap: 10px;
    align-items: start;
  }
  .add-form .field { display: flex; flex-direction: column; gap: 4px; }
  .add-form label {
    font-size: 12px;
    color: var(--muted);
    font-weight: 500;
  }
  .form-error {
    grid-column: 1 / -1;
    color: #dc2626;
    font-size: 12px;
    min-height: 16px;
  }
  .add-form .submit-wrap {
    display: flex;
    align-items: end;
    height: 100%;
  }

  .board {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
  }
  .column {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    min-height: 300px;
    box-shadow: var(--shadow);
  }
  .column-header {
    padding: 12px 14px;
    border-bottom: 1px solid var(--border);
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .column-title {
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.3px;
  }
  .column-count {
    background: #f3f4f6;
    color: var(--muted);
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 12px;
    font-weight: 500;
  }
  .column-body {
    padding: 10px;
    flex: 1;
    min-height: 200px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    transition: background 0.15s;
  }
  .column-body.drag-over {
    background: #fef3f2;
  }
  .empty-state {
    color: #9ca3af;
    text-align: center;
    padding: 30px 12px;
    font-size: 12px;
    border: 1px dashed var(--border);
    border-radius: 6px;
  }

  .card {
    background: #fff;
    border: 1px solid var(--border);
    border-left: 3px solid var(--p-mid);
    border-radius: 6px;
    padding: 10px 12px;
    cursor: grab;
    box-shadow: var(--shadow);
    transition: transform 0.1s, box-shadow 0.15s;
  }
  .card:hover { box-shadow: 0 2px 4px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.1); }
  .card.dragging { opacity: 0.5; cursor: grabbing; }
  .card[data-priority="高"] { border-left-color: var(--p-high); }
  .card[data-priority="中"] { border-left-color: var(--p-mid); }
  .card[data-priority="低"] { border-left-color: var(--p-low); }

  .card-title {
    font-size: 13px;
    font-weight: 500;
    margin-bottom: 8px;
    word-wrap: break-word;
    line-height: 1.4;
  }
  .card-meta {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    font-size: 11px;
    color: var(--muted);
    margin-bottom: 8px;
  }
  .badge {
    background: #f3f4f6;
    padding: 2px 8px;
    border-radius: 10px;
    font-weight: 500;
  }
  .badge.priority-高 { background: #fef2f2; color: #dc2626; }
  .badge.priority-中 { background: #fffbeb; color: #d97706; }
  .badge.priority-低 { background: #eff6ff; color: #2563eb; }

  .card-actions {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
  }
  .card-actions button {
    padding: 4px 8px;
    font-size: 12px;
  }

  .timer {
    margin-top: 8px;
    padding: 8px;
    background: #fafafa;
    border-radius: 4px;
    border: 1px solid var(--border);
  }
  .timer-display {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 18px;
    font-weight: 600;
    text-align: center;
    margin-bottom: 6px;
    color: var(--accent);
  }
  .timer-display.done { color: #059669; }
  .timer-controls {
    display: flex;
    gap: 4px;
    justify-content: center;
  }
  .timer-controls button {
    padding: 3px 8px;
    font-size: 11px;
  }

  @media (max-width: 900px) {
    .board { grid-template-columns: 1fr; }
    .add-form { grid-template-columns: 1fr 1fr; }
    .add-form .field:first-child { grid-column: 1 / -1; }
    .add-form .submit-wrap { grid-column: 1 / -1; }
    .add-form .submit-wrap button { width: 100%; }
  }
  @media (max-width: 480px) {
    header { padding: 12px; }
    main { padding: 12px; }
    h1 { font-size: 15px; width: 100%; }
    .toolbar { margin-left: 0; width: 100%; }
    .search { flex: 1; width: auto; min-width: 0; }
    .add-form { grid-template-columns: 1fr; padding: 12px; }
    .add-form .field:first-child { grid-column: auto; }
  }
</style>
</head>
<body>
<header>
  <div class="header-inner">
    <h1>???? 番茄任务看板</h1>
    <div class="toolbar">
      <input type="text" id="search" class="search" placeholder="搜索任务标题…" aria-label="搜索">
      <label class="switch" title="开启后 25 分钟将以 25 秒运行">
        <input type="checkbox" id="demoMode">
        <span>演示加速</span>
      </label>
      <button id="clearDone" class="danger">清空已完成</button>
    </div>
  </div>
</header>

<main>
  <form class="add-form" id="addForm" autocomplete="off">
    <div class="field">
      <label for="title">任务标题</label>
      <input type="text" id="title" placeholder="例如:实现登录页接口对接" maxlength="100">
    </div>
    <div class="field">
      <label for="estimate">预估番茄数</label>
      <input type="number" id="estimate" min="1" max="20" value="1">
    </div>
    <div class="field">
      <label for="priority">优先级</label>
      <select id="priority">
        <option value="高">高</option>
        <option value="中" selected>中</option>
        <option value="低">低</option>
      </select>
    </div>
    <div class="submit-wrap">
      <button type="submit" class="primary">添加任务</button>
    </div>
    <div class="form-error" id="formError" aria-live="polite"></div>
  </form>

  <div class="board" id="board">
    <div class="column" data-status="todo">
      <div class="column-header">
        <span class="column-title">待办</span>
        <span class="column-count" data-count="todo">0</span>
      </div>
      <div class="column-body" data-drop="todo"></div>
    </div>
    <div class="column" data-status="doing">
      <div class="column-header">
        <span class="column-title">进行中</span>
        <span class="column-count" data-count="doing">0</span>
      </div>
      <div class="column-body" data-drop="doing"></div>
    </div>
    <div class="column" data-status="done">
      <div class="column-header">
        <span class="column-title">已完成</span>
        <span class="column-count" data-count="done">0</span>
      </div>
      <div class="column-body" data-drop="done"></div>
    </div>
  </div>
</main>

<script>
(function() {
  const STORAGE_KEY = 'pomodoro_kanban_v1';
  const DEMO_KEY = 'pomodoro_kanban_demo';
  const FULL_DURATION = 25 * 60;
  const DEMO_DURATION = 25;

  let tasks = [];
  let searchQuery = '';
  let demoMode = false;
  const timers = {}; // id -> { remaining, running, intervalId, total }

  function loadState() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (raw) tasks = JSON.parse(raw) || [];
    } catch(e) { tasks = []; }
    demoMode = localStorage.getItem(DEMO_KEY) === '1';
    document.getElementById('demoMode').checked = demoMode;
  }
  function saveState() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
  }

  function uid() {
    return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
  }

  function getDuration() {
    return demoMode ? DEMO_DURATION : FULL_DURATION;
  }

  function formatTime(s) {
    const m = Math.floor(s / 60);
    const sec = s % 60;
    return String(m).padStart(2,'0') + ':' + String(sec).padStart(2,'0');
  }

  function showError(msg) {
    document.getElementById('formError').textContent = msg || '';
  }

  function addTask(data) {
    tasks.push({
      id: uid(),
      title: data.title,
      estimate: data.estimate,
      priority: data.priority,
      status: 'todo',
      completed: 0,
      createdAt: Date.now()
    });
    saveState();
    render();
  }

  function deleteTask(id) {
    if (timers[id]) {
      clearInterval(timers[id].intervalId);
      delete timers[id];
    }
    tasks = tasks.filter(t => t.id !== id);
    saveState();
    render();
  }

  function updateStatus(id, status) {
    const t = tasks.find(x => x.id === id);
    if (t && t.status !== status) {
      t.status = status;
      saveState();
      render();
    }
  }

  function clearDone() {
    const doneTasks = tasks.filter(t => t.status === 'done');
    if (doneTasks.length === 0) {
      showError('没有可清空的已完成任务');
      setTimeout(() => showError(''), 2000);
      return;
    }
    if (!confirm(`确定清空 ${doneTasks.length} 个已完成任务?`)) return;
    doneTasks.forEach(t => {
      if (timers[t.id]) {
        clearInterval(timers[t.id].intervalId);
        delete timers[t.id];
      }
    });
    tasks = tasks.filter(t => t.status !== 'done');
    saveState();
    render();
  }

  // Timer logic
  function startTimer(id) {
    if (!timers[id]) {
      timers[id] = { remaining: getDuration(), running: false, intervalId: null, total: getDuration() };
    }
    const t = timers[id];
    if (t.running) return;
    t.running = true;
    t.intervalId = setInterval(() => {
      t.remaining--;
      if (t.remaining <= 0) {
        clearInterval(t.intervalId);
        t.running = false;
        t.remaining = 0;
        const task = tasks.find(x => x.id === id);
        if (task) {
          task.completed = (task.completed || 0) + 1;
          saveState();
        }
        try {
          // simple beep using audio context (no external)
          const ctx = new (window.AudioContext || window.webkitAudioContext)();
          const o = ctx.createOscillator();
          const g = ctx.createGain();
          o.connect(g); g.connect(ctx.destination);
          o.frequency.value = 880;
          g.gain.setValueAtTime(0.1, ctx.currentTime);
          g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
          o.start(); o.stop(ctx.currentTime + 0.5);
        } catch(e) {}
        render();
        return;
      }
      updateTimerDisplay(id);
    }, 1000);
    render();
  }

  function pauseTimer(id) {
    const t = timers[id];
    if (!t) return;
    clearInterval(t.intervalId);
    t.running = false;
    render();
  }

  function resetTimer(id) {
    const t = timers[id];
    if (t) {
      clearInterval(t.intervalId);
      delete timers[id];
    }
    render();
  }

  function updateTimerDisplay(id) {
    const el = document.querySelector(`[data-timer-display="${id}"]`);
    if (el && timers[id]) {
      el.textContent = formatTime(timers[id].remaining);
    }
  }

  // Drag & drop
  let draggedId = null;

  function attachCardDragHandlers(cardEl) {
    cardEl.addEventListener('dragstart', e => {
      draggedId = cardEl.dataset.id;
      cardEl.classList.add('dragging');
      e.dataTransfer.effectAllowed = 'move';
      e.dataTransfer.setData('text/plain', draggedId);
    });
    cardEl.addEventListener('dragend', () => {
      cardEl.classList.remove('dragging');
      draggedId = null;
    });
  }

  function attachColumnDropHandlers() {
    document.querySelectorAll('[data-drop]').forEach(col => {
      col.addEventListener('dragover', e => {
        e.preventDefault();
        e.dataTransfer.dropEffect = 'move';
        col.classList.add('drag-over');
      });
      col.addEventListener('dragleave', () => col.classList.remove('drag-over'));
      col.addEventListener('drop', e => {
        e.preventDefault();
        col.classList.remove('drag-over');
        const id = draggedId || e.dataTransfer.getData('text/plain');
        if (id) updateStatus(id, col.dataset.drop);
      });
    });
  }

  // Render
  function render() {
    const q = searchQuery.trim().toLowerCase();
    const statuses = ['todo', 'doing', 'done'];
    statuses.forEach(s => {
      const body = document.querySelector(`[data-drop="${s}"]`);
      const list = tasks
        .filter(t => t.status === s)
        .filter(t => !q || t.title.toLowerCase().includes(q))
        .sort((a,b) => a.createdAt - b.createdAt);
      body.innerHTML = '';
      document.querySelector(`[data-count="${s}"]`).textContent = tasks.filter(t => t.status === s).length;
      if (list.length === 0) {
        const empty = document.createElement('div');
        empty.className = 'empty-state';
        if (q) {
          empty.textContent = '无匹配任务';
        } else if (s === 'todo') {
          empty.textContent = '暂无待办任务,添加一个开始吧';
        } else if (s === 'doing') {
          empty.textContent = '拖动卡片到这里开始进行';
        } else {
          empty.textContent = '完成的任务会显示在这里';
        }
        body.appendChild(empty);
        return;
      }
      list.forEach(t => body.appendChild(renderCard(t)));
    });
  }

  function renderCard(t) {
    const card = document.createElement('div');
    card.className = 'card';
    card.draggable = true;
    card.dataset.id = t.id;
    card.dataset.priority = t.priority;

    const title = document.createElement('div');
    title.className = 'card-title';
    title.textContent = t.title;
    card.appendChild(title);

    const meta = document.createElement('div');
    meta.className = 'card-meta';
    const pBadge = document.createElement('span');
    pBadge.className = 'badge priority-' + t.priority;
    pBadge.textContent = '优先级:' + t.priority;
    meta.appendChild(pBadge);
    const eBadge = document.createElement('span');
    eBadge.className = 'badge';
    eBadge.textContent = `???? ${t.completed||0}/${t.estimate}`;
    meta.appendChild(eBadge);
    card.appendChild(meta);

    const actions = document.createElement('div');
    actions.className = 'card-actions';
    const startBtn = document.createElement('button');
    startBtn.textContent = timers[t.id] && timers[t.id].running ? '运行中' : '开始 25 分钟番茄钟';
    startBtn.disabled = !!(timers[t.id] && timers[t.id].running);
    startBtn.addEventListener('click', () => startTimer(t.id));
    actions.appendChild(startBtn);

    const delBtn = document.createElement('button');
    delBtn.textContent = '删除';
    delBtn.className = 'danger';
    delBtn.addEventListener('click', () => {
      if (confirm('确定删除该任务?')) deleteTask(t.id);
    });
    actions.appendChild(delBtn);
    card.appendChild(actions);

    if (timers[t.id]) {
      const tm = timers[t.id];
      const timerDiv = document.createElement('div');
      timerDiv.className = 'timer';
      const disp = document.createElement('div');
      disp.className = 'timer-display' + (tm.remaining === 0 ? ' done' : '');
      disp.dataset.timerDisplay = t.id;
      disp.textContent = tm.remaining === 0 ? '完成 ✓' : formatTime(tm.remaining);
      timerDiv.appendChild(disp);

      const ctrls = document.createElement('div');
      ctrls.className = 'timer-controls';
      if (tm.remaining > 0) {
        if (tm.running) {
          const pb = document.createElement('button');
          pb.textContent = '暂停';
          pb.addEventListener('click', () => pauseTimer(t.id));
          ctrls.appendChild(pb);
        } else {
          const cb = document.createElement('button');
          cb.textContent = '继续';
          cb.addEventListener('click', () => startTimer(t.id));
          ctrls.appendChild(cb);
        }
      }
      const rb = document.createElement('button');
      rb.textContent = '重置';
      rb.addEventListener('click', () => resetTimer(t.id));
      ctrls.appendChild(rb);
      timerDiv.appendChild(ctrls);
      card.appendChild(timerDiv);
    }

    attachCardDragHandlers(card);
    return card;
  }

  // Events
  document.getElementById('addForm').addEventListener('submit', e => {
    e.preventDefault();
    const title = document.getElementById('title').value.trim();
    const estimate = parseInt(document.getElementById('estimate').value, 10);
    const priority = document.getElementById('priority').value;
    if (!title) {
      showError('请填写任务标题');
      return;
    }
    if (title.length > 100) {
      showError('标题过长(最多 100 字)');
      return;
    }
    if (!estimate || estimate < 1 || estimate > 20) {
      showError('预估番茄数应在 1 到 20 之间');
      return;
    }
    if (!['高','中','低'].includes(priority)) {
      showError('优先级无效');
      return;
    }
    showError('');
    addTask({ title, estimate, priority });
    document.getElementById('title').value = '';
    document.getElementById('estimate').value = '1';
    document.getElementById('priority').value = '中';
    document.getElementById('title').focus();
  });

  document.getElementById('search').addEventListener('input', e => {
    searchQuery = e.target.value;
    render();
  });

  document.getElementById('clearDone').addEventListener('click', clearDone);

  document.getElementById('demoMode').addEventListener('change', e => {
    demoMode = e.target.checked;
    localStorage.setItem(DEMO_KEY, demoMode ? '1' : '0');
  });

  // Init
  loadState();
  attachColumnDropHandlers();
  render();
})();
</script>
</body>
</html>
有真实输出 完整 HTML 包含交互脚本 无外部资源 无 Markdown 包裹

单文件 HTML:发光粒子按钮

浏览器可直接预览的 Coding 题。主要看是否能按要求输出完整 HTML、交互是否存在、是否引入外部资源。

看完整现场

OfoxAI

ofoxClaudeOpus4.8 · anthropic/claude-opus-4.8

未交卷

这个 AI 服务还没有公开跑过这道题。

OfoxAI

ofoxOpenAIGPT5.5 · openai/gpt-5.5

未交卷

这个 AI 服务还没有公开跑过这道题。

PackyAPI

packyApiClaudeOpus4.7 · claude-opus-4-7

疑似截断

2026-05-23 15:06:16 · 18074 ms · 输入 228 / 输出 1,200 / 总计 1,428 tokens

查看完整源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Glowing Ripple Button</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    height: 100%;
    width: 100%;
    overflow: hidden;
    background: radial-gradient(circle at center, #1a1a3a 0%, #05050f 100%);
    font-family: 'Segoe UI', Tahoma, sans-serif;
  }
  body {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .ripple-btn {
    position: relative;
    padding: 18px 48px;
    font-size: 1.2rem;
    font-weight: 600;
    letter-spacing: 2px;
    color: #fff;
    background: linear-gradient(135deg, #6a11cb, #2575fc);
    border: none;
    border-radius: 50px;
    cursor: pointer;
    outline: none;
    box-shadow:
      0 0 20px rgba(101, 60, 255, 0.6),
      0 0 40px rgba(37, 117, 252, 0.4),
      inset 0 0 12px rgba(255, 255, 255, 0.15);
    transition: transform 0.15s ease, box-shadow 0.3s ease;
  }
  .ripple-btn:hover {
    transform: scale(1.05);
    box-shadow:
      0 0 30px rgba(101, 60, 255, 0.8),
      0 0 60px rgba(37, 117, 252, 0.6),
      inset 0 0 12px rgba(255, 255, 255, 0.25);
  }
  .ripple-btn:active {
    transform: scale(0.97);
  }
  .particle {
    position: fixed;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    pointer-events: none;
    background: #fff;
    box-shadow: 0 0 8px 2px currentColor;
    animation: fly 700ms cubic-bezier(0.15, 0.7, 0.3, 1) forwards;
    will-change: transform, opacity;
  }
  @keyframes fly {
    0%   { transform: translate(-50%, -50%) translate(0, 0) scale(1); opacity: 1; }
    100% { transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) scale(0.2); opacity: 0; }
  }
</style>
</head>
<body>
  <button class="ripple-btn" id="btn">CLICK ME</button>
  <script>
    const btn = document.getElementById('btn');
    const colors = ['#ff4dd2', '#4dd0ff', '#ffd84d', '#a64dff', '#4dffb8', '#ff7a4d'];

    btn.addEventListener('click', (e) => {
      const rect = btn.getBoundingClientRect();
      const cx = rect.left + rect.width / 2;
      const cy = rect.top + rect.height / 2;
      const count = 24;

      for (let i = 0; i < count; i++) {
        const p = document.createElement('div');
        p.className = 'particle';
        const angle = (Math.PI * 2 * i) / count + (Math.random() * 0.4 - 0.2);
        const distance = 80 + Math.random() * 100;
        const dx = Math.cos(angle) * distance;
        const dy = Math.sin(angle) * distance;
        const size = 4 + Math.random
有真实输出 完整 HTML 包含交互脚本 无外部资源 无 Markdown 包裹

OfoxAI

ofoxClaudeOpus4.7 · anthropic/claude-opus-4.7

疑似截断

2026-05-23 16:41:31 · 8474 ms · 输入 228 / 输出 1,199 / 总计 1,427 tokens

查看完整源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Glowing Ripple Button</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    height: 100%;
    width: 100%;
    overflow: hidden;
    background: radial-gradient(circle at center, #1a1a2e 0%, #0a0a14 100%);
    font-family: 'Segoe UI', system-ui, sans-serif;
  }
  body {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .ripple-btn {
    position: relative;
    padding: 18px 46px;
    font-size: 18px;
    font-weight: 600;
    letter-spacing: 1.5px;
    color: #fff;
    background: linear-gradient(135deg, #ff3cac, #784ba0, #2b86c5);
    border: none;
    border-radius: 50px;
    cursor: pointer;
    outline: none;
    text-transform: uppercase;
    box-shadow:
      0 0 20px rgba(255, 60, 172, 0.4),
      0 0 40px rgba(120, 75, 160, 0.3),
      inset 0 0 10px rgba(255, 255, 255, 0.1);
    transition: transform 0.15s ease, box-shadow 0.3s ease;
  }
  .ripple-btn:hover {
    transform: scale(1.05);
    box-shadow:
      0 0 30px rgba(255, 60, 172, 0.6),
      0 0 60px rgba(120, 75, 160, 0.5),
      inset 0 0 15px rgba(255, 255, 255, 0.2);
  }
  .ripple-btn:active {
    transform: scale(0.97);
  }
  .particle {
    position: fixed;
    pointer-events: none;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 0 10px 2px currentColor;
    will-change: transform, opacity;
    animation: particle-fly 700ms ease-out forwards;
  }
  @keyframes particle-fly {
    0% {
      opacity: 1;
      transform: translate(-50%, -50%) translate(0, 0) scale(1);
    }
    100% {
      opacity: 0;
      transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) scale(0.3);
    }
  }
</style>
</head>
<body>
  <button class="ripple-btn" id="rippleBtn">Click Me</button>
<script>
  const btn = document.getElementById('rippleBtn');
  const colors = ['#ff3cac', '#ffd166', '#06d6a0', '#2b86c5', '#f72585', '#7209b7', '#fff'];

  btn.addEventListener('click', (e) => {
    const rect = btn.getBoundingClientRect();
    const cx = rect.left + rect.width / 2;
    const cy = rect.top + rect.height / 2;
    const count = 18;

    for (let i = 0; i < count; i++) {
      const p = document.createElement('div');
      p.className = 'particle';
      const angle = (Math.PI * 2 * i) / count + Math.random() * 0.4;
      const distance = 80 + Math.random() * 80;
      const dx = Math.cos(angle) * distance;
      const dy = Math.sin(angle) * distance;
有真实输出 完整 HTML 包含交互脚本 无外部资源 无 Markdown 包裹