
<!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>