番茄时钟

经验创意 · 143 次浏览
困困君 创建于 2026-05-19 00:14

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html,body{
  margin:0;
  width:100%;
  height:100%;
  overflow:hidden;
  background:transparent;
  font-family:"Microsoft YaHei",sans-serif;
}
*{box-sizing:border-box}
.card{
  width:100%;
  height:100%;
  padding:8px;
  border-radius:18px;
  border:1px solid rgba(120,190,255,.32);
  background:
    radial-gradient(circle at 12% 0%,rgba(87,185,255,.23),transparent 42%),
    radial-gradient(circle at 100% 100%,rgba(255,95,130,.18),transparent 44%),
    linear-gradient(145deg,#111827,#07111f 62%,#050913);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.14),
    inset 0 -18px 35px rgba(0,0,0,.18);
  color:#eaf6ff;
  display:flex;
  flex-direction:column;
  gap:5px;
  overflow:hidden;
  position:relative;
}
.card.flash{
  animation:flashBg .55s ease-in-out 6;
}
@keyframes flashBg{
  0%,100%{filter:brightness(1)}
  50%{filter:brightness(1.65)}
}
.top{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:6px;
  height:16px;
  min-height:16px;
}
.title{
  font-size:12px;
  font-weight:700;
  letter-spacing:.5px;
  white-space:nowrap;
}
.mode{
  font-size:9px;
  color:rgba(206,233,255,.62);
  white-space:nowrap;
}
.time{
  flex:1;
  min-height:32px;
  display:flex;
  align-items:center;
  justify-content:center;
  font-size:30px;
  line-height:1;
  font-weight:800;
  letter-spacing:1px;
  color:#f4fbff;
  text-shadow:0 0 18px rgba(105,200,255,.26);
  font-variant-numeric:tabular-nums;
}
.progress{
  height:5px;
  border-radius:999px;
  overflow:hidden;
  background:rgba(255,255,255,.08);
  border:1px solid rgba(145,207,255,.13);
}
.bar{
  height:100%;
  width:100%;
  border-radius:999px;
  background:linear-gradient(90deg,rgba(96,190,255,.85),rgba(255,111,145,.78));
  transition:width .25s linear;
}
.actions{
  display:grid;
  grid-template-columns:1fr 1fr 1fr;
  gap:5px;
  height:24px;
  min-height:24px;
}
button{
  height:24px;
  border:0;
  outline:none;
  border-radius:9px;
  font-family:"Microsoft YaHei",sans-serif;
  font-size:11px;
  font-weight:700;
  color:#dff4ff;
  background:linear-gradient(180deg,rgba(79,163,255,.30),rgba(42,96,166,.20));
  border:1px solid rgba(139,208,255,.30);
  cursor:pointer;
  transition:transform .08s ease,background .12s ease,border-color .12s ease;
}
button:hover{
  background:linear-gradient(180deg,rgba(95,185,255,.44),rgba(58,120,196,.28));
  border-color:rgba(158,222,255,.58);
}
button:active{
  transform:scale(.96);
  background:linear-gradient(180deg,rgba(55,128,210,.35),rgba(28,68,126,.32));
}
button.danger{
  background:linear-gradient(180deg,rgba(255,111,145,.28),rgba(158,55,88,.20));
  border-color:rgba(255,145,174,.28);
}
.status{
  height:10px;
  line-height:10px;
  font-size:9px;
  color:rgba(207,235,255,.50);
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
::-webkit-scrollbar{width:5px;height:5px}
::-webkit-scrollbar-thumb{background:rgba(135,190,255,.28);border-radius:999px}
::-webkit-scrollbar-track{background:rgba(255,255,255,.04);border-radius:999px}
</style>
</head>
<body>
<div class="card" id="card">
  <div class="top">
    <div class="title">番茄时钟</div>
    <div class="mode" id="modeText">专注</div>
  </div>
  <div class="time" id="timeText">25:00</div>
  <div class="progress"><div class="bar" id="bar"></div></div>
  <div class="actions">
    <button id="startBtn">开始</button>
    <button id="switchBtn">休息</button>
    <button class="danger" id="resetBtn">重置</button>
  </div>
  <div class="status" id="status">25 分钟专注,完成后提醒</div>
</div>
<script>
(function(){
  var yanm=null;
  var ready=false;
  var tries=0;
  var timer=null;

  var card=document.getElementById("card");
  var modeText=document.getElementById("modeText");
  var timeText=document.getElementById("timeText");
  var bar=document.getElementById("bar");
  var status=document.getElementById("status");
  var startBtn=document.getElementById("startBtn");
  var switchBtn=document.getElementById("switchBtn");
  var resetBtn=document.getElementById("resetBtn");

  var WORK=25*60;
  var REST=5*60;

  var state={
    mode:"work",
    running:false,
    remaining:WORK,
    total:WORK,
    endAt:0,
    completed:0
  };

  function setStatus(t){
    status.textContent=t || "";
  }

  function hasHost(){
    return window.yanm && typeof window.yanm.invoke==="function";
  }

  function invoke(method,args){
    if(!yanm){
      return Promise.reject(new Error("燕幕宿主未就绪"));
    }
    return yanm.invoke(method,args || {});
  }

  function retryInit(){
    if(hasHost()){
      yanm=window.yanm;
      ready=true;
      loadState();
      return;
    }
    tries++;
    if(tries<80){
      setTimeout(retryInit,120);
    }
  }

  function loadState(){
    invoke("state.read",{key:"pomodoroState",defaultValue:"{}"}).then(function(res){
      var text=typeof res==="string" ? res : "{}";
      try{
        var obj=JSON.parse(text || "{}");
        if(obj && typeof obj==="object"){
          if(obj.mode==="work" || obj.mode==="rest") state.mode=obj.mode;
          if(typeof obj.running==="boolean") state.running=obj.running;
          if(typeof obj.remaining==="number") state.remaining=obj.remaining;
          if(typeof obj.total==="number") state.total=obj.total;
          if(typeof obj.endAt==="number") state.endAt=obj.endAt;
          if(typeof obj.completed==="number") state.completed=obj.completed;
        }
      }catch(e){}
      if(state.running && state.endAt>0){
        state.remaining=Math.max(0,Math.round((state.endAt-Date.now())/1000));
        if(state.remaining<=0){
          finishRound();
          return;
        }
        startTick();
      }
      render();
      setStatus("状态已读取");
    }).catch(function(){
      render();
      setStatus("本地计时模式");
    });
  }

  function saveState(){
    if(!ready)return;
    invoke("state.write",{
      key:"pomodoroState",
      value:JSON.stringify(state)
    }).catch(function(){});
  }

  function pad(n){
    return n<10 ? "0"+n : ""+n;
  }

  function render(){
    var m=Math.floor(state.remaining/60);
    var s=state.remaining%60;
    timeText.textContent=pad(m)+":"+pad(s);
    modeText.textContent=state.mode==="work" ? "专注" : "休息";
    switchBtn.textContent=state.mode==="work" ? "休息" : "专注";
    startBtn.textContent=state.running ? "暂停" : "开始";
    var percent=state.total>0 ? Math.max(0,Math.min(100,state.remaining/state.total*100)) : 0;
    bar.style.width=percent+"%";
    if(state.running){
      setStatus((state.mode==="work" ? "专注中" : "休息中") + " · 已完成 " + state.completed + " 次");
    }else{
      setStatus((state.mode==="work" ? "准备专注" : "准备休息") + " · 已完成 " + state.completed + " 次");
    }
  }

  function startTick(){
    clearInterval(timer);
    timer=setInterval(function(){
      if(!state.running)return;
      state.remaining=Math.max(0,Math.round((state.endAt-Date.now())/1000));
      if(state.remaining<=0){
        finishRound();
        return;
      }
      render();
    },300);
  }

  function start(){
    if(state.running){
      pause();
      return;
    }
    state.running=true;
    state.endAt=Date.now()+state.remaining*1000;
    startTick();
    render();
    saveState();
  }

  function pause(){
    state.running=false;
    state.endAt=0;
    clearInterval(timer);
    render();
    saveState();
  }

  function reset(){
    state.running=false;
    state.endAt=0;
    clearInterval(timer);
    state.total=state.mode==="work" ? WORK : REST;
    state.remaining=state.total;
    render();
    saveState();
  }

  function switchMode(){
    state.mode=state.mode==="work" ? "rest" : "work";
    state.running=false;
    state.endAt=0;
    clearInterval(timer);
    state.total=state.mode==="work" ? WORK : REST;
    state.remaining=state.total;
    render();
    saveState();
  }

  function finishRound(){
    clearInterval(timer);
    state.running=false;
    state.endAt=0;
    state.remaining=0;
    if(state.mode==="work"){
      state.completed++;
    }
    render();
    notifyDone();
    saveState();
    setTimeout(function(){
      state.mode=state.mode==="work" ? "rest" : "work";
      state.total=state.mode==="work" ? WORK : REST;
      state.remaining=state.total;
      render();
      saveState();
    },900);
  }

  function notifyDone(){
    card.classList.remove("flash");
    void card.offsetWidth;
    card.classList.add("flash");
    setStatus(state.mode==="work" ? "专注完成,休息一下" : "休息结束,继续专注");

    try{
      var AudioContext=window.AudioContext || window.webkitAudioContext;
      var ctx=new AudioContext();
      var now=ctx.currentTime;
      beep(ctx,now,880,.13);
      beep(ctx,now+.18,1175,.16);
      beep(ctx,now+.40,740,.20);
      setTimeout(function(){ctx.close && ctx.close();},900);
    }catch(e){}
  }

  function beep(ctx,start,freq,dur){
    var osc=ctx.createOscillator();
    var gain=ctx.createGain();
    osc.type="sine";
    osc.frequency.value=freq;
    gain.gain.setValueAtTime(0.0001,start);
    gain.gain.exponentialRampToValueAtTime(0.18,start+.02);
    gain.gain.exponentialRampToValueAtTime(0.0001,start+dur);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start(start);
    osc.stop(start+dur+.02);
  }

  startBtn.addEventListener("click",start);
  resetBtn.addEventListener("click",reset);
  switchBtn.addEventListener("click",switchMode);

  render();
  retryInit();
})();
</script>
</body>
</html>

困困君 最后更新于 2026/5/19

回复内容
暂无回复
回复主贴