anthropic/claude-opus-4.8
该模型能力由 OfoxAI 站点接入
查看原始输出
<!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>
2026-05-30 15:45:10 · 86273 ms · 总耗时 86s · 输入 653 / 输出 9,760 / 总计 10,413 tokens