Files
my-page/src/pages/lab/Pomodoro.vue
2026-08-13 21:14:30 +08:00

301 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
const KEY = 'pomodoro-stats-v1'
const MODES = {
focus: { name: '专注', mins: 25, color: '#fb7185' },
short: { name: '小休', mins: 5, color: '#5eead4' },
long: { name: '大休', mins: 15, color: '#818cf8' },
}
const mode = ref('focus')
const left = ref(MODES.focus.mins * 60)
const running = ref(false)
const stats = ref({ done: 0, minutes: 0, days: {} })
let timer = null
function loadStats() {
try {
const s = JSON.parse(localStorage.getItem(KEY))
if (s) stats.value = s
} catch { /* 使用默认 */ }
}
function saveStats() {
try {
localStorage.setItem(KEY, JSON.stringify(stats.value))
} catch { /* 忽略 */ }
}
function switchMode(m) {
mode.value = m
running.value = false
left.value = MODES[m].mins * 60
}
function toggle() {
running.value = !running.value
}
function reset() {
running.value = false
left.value = MODES[mode.value].mins * 60
}
function finish() {
running.value = false
if (mode.value === 'focus') {
stats.value.done++
stats.value.minutes += MODES.focus.mins
const day = new Date().toISOString().slice(0, 10)
stats.value.days[day] = (stats.value.days[day] || 0) + 1
saveStats()
switchMode(stats.value.done % 4 === 0 ? 'long' : 'short')
} else {
switchMode('focus')
}
}
onMounted(() => {
loadStats()
timer = setInterval(() => {
if (!running.value) return
left.value--
if (left.value <= 0) finish()
}, 1000)
})
onBeforeUnmount(() => clearInterval(timer))
const mm = computed(() => String(Math.floor(left.value / 60)).padStart(2, '0'))
const ss = computed(() => String(left.value % 60).padStart(2, '0'))
const total = computed(() => MODES[mode.value].mins * 60)
const pct = computed(() => 1 - left.value / total.value)
const today = computed(() => stats.value.days[new Date().toISOString().slice(0, 10)] || 0)
const ringStyle = computed(() => ({
background: `conic-gradient(${MODES[mode.value].color} ${pct.value * 360}deg, rgba(255,255,255,.07) 0)`,
}))
</script>
<template>
<div class="po">
<nav class="lab-nav">
<router-link to="/lab" class="lab-back">← 实验室</router-link>
<h1 class="lab-title">番茄钟</h1>
<span class="lab-badge">FOCUS</span>
</nav>
<p class="lab-desc">25 分钟专注 + 5 分钟小休,每四个番茄换一次大休;完成记录存在本地,关掉页面也不丢。</p>
<div class="po-stage">
<div class="po-modes">
<button
v-for="(m, k) in MODES"
:key="k"
:class="{ on: mode === k }"
:style="mode === k ? { background: m.color, borderColor: m.color } : {}"
@click="switchMode(k)"
>
{{ m.name }} {{ m.mins }}′
</button>
</div>
<div class="po-ring" :style="ringStyle">
<div class="po-face">
<p class="po-time">{{ mm }}:{{ ss }}</p>
<p class="po-state" :style="{ color: MODES[mode].color }">
{{ running ? '进行中 · ' + MODES[mode].name : '已暂停' }}
</p>
</div>
</div>
<div class="po-actions">
<button class="po-main" :style="{ background: MODES[mode].color }" @click="toggle">
{{ running ? '⏸ 暂停' : '▶ 开始' }}
</button>
<button class="po-sub" @click="reset">↺ 重置</button>
</div>
<div class="po-stats">
<div class="po-stat"><b>{{ today }}</b><span>今日番茄</span></div>
<div class="po-stat"><b>{{ stats.done }}</b><span>累计完成</span></div>
<div class="po-stat"><b>{{ Math.round(stats.minutes / 6) / 10 }}</b><span>专注小时</span></div>
</div>
<div class="po-dots">
<i v-for="n in 8" :key="n" :class="{ on: n <= (stats.done % 8 || (stats.done ? 8 : 0)) }"></i>
<span>本轮进度(8 个一大轮)</span>
</div>
</div>
</div>
</template>
<style scoped>
.po {
min-height: 100vh;
background: #0b0e14;
color: #e8ecf4;
font-family: 'Space Grotesk', 'Noto Serif SC', sans-serif;
padding: 24px clamp(16px, 5vw, 64px) 60px;
}
.lab-nav {
display: flex;
align-items: center;
gap: 16px;
max-width: 760px;
margin: 0 auto 10px;
}
.lab-back {
color: #f59e0b;
text-decoration: none;
font-size: 13px;
border: 1px solid rgba(245, 158, 11, 0.4);
border-radius: 8px;
padding: 7px 14px;
}
.lab-back:hover { background: rgba(245, 158, 11, 0.12); }
.lab-title { margin: 0; font-size: 22px; font-weight: 700; }
.lab-badge {
font-size: 10px;
letter-spacing: 0.22em;
color: #5eead4;
border: 1px solid rgba(94, 234, 212, 0.4);
border-radius: 99px;
padding: 4px 10px;
}
.lab-desc {
max-width: 760px;
margin: 0 auto 34px;
font-size: 13.5px;
color: rgba(232, 236, 244, 0.66);
line-height: 1.8;
}
.po-stage {
max-width: 760px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.po-modes {
display: flex;
gap: 8px;
margin-bottom: 34px;
}
.po-modes button {
background: #13161f;
border: 1px solid #2a2f3d;
border-radius: 99px;
color: rgba(232, 236, 244, 0.72);
font-size: 13px;
padding: 9px 20px;
cursor: pointer;
transition: all 0.2s ease;
}
.po-modes button.on {
color: #0b0e14;
font-weight: 700;
}
.po-ring {
width: min(300px, 74vw);
aspect-ratio: 1;
border-radius: 50%;
padding: 12px;
display: grid;
place-items: center;
transition: background 0.4s ease;
}
.po-face {
width: 100%;
height: 100%;
border-radius: 50%;
background: #10131c;
display: grid;
place-content: center;
text-align: center;
gap: 6px;
}
.po-time {
margin: 0;
font-size: clamp(52px, 12vw, 68px);
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
.po-state {
margin: 0;
font-size: 12.5px;
letter-spacing: 0.2em;
}
.po-actions {
display: flex;
gap: 12px;
margin: 30px 0 34px;
}
.po-main {
border: none;
border-radius: 999px;
color: #0b0e14;
font-size: 15px;
font-weight: 700;
padding: 14px 40px;
cursor: pointer;
transition: transform 0.15s ease, background 0.3s ease;
}
.po-main:hover { transform: scale(1.05); }
.po-sub {
background: #13161f;
border: 1px solid #2a2f3d;
border-radius: 999px;
color: rgba(232, 236, 244, 0.72);
font-size: 14px;
padding: 14px 24px;
cursor: pointer;
}
.po-stats {
display: flex;
gap: 12px;
width: 100%;
max-width: 480px;
}
.po-stat {
flex: 1;
background: #10131c;
border: 1px solid #222736;
border-radius: 14px;
padding: 16px 10px;
text-align: center;
}
.po-stat b {
display: block;
font-size: 26px;
color: #fb7185;
font-variant-numeric: tabular-nums;
}
.po-stat span {
font-size: 11.5px;
color: rgba(232, 236, 244, 0.55);
}
.po-dots {
display: flex;
align-items: center;
gap: 7px;
margin-top: 24px;
}
.po-dots i {
width: 11px;
height: 11px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
.po-dots i.on {
background: #fb7185;
box-shadow: 0 0 10px rgba(251, 113, 133, 0.6);
}
.po-dots span {
margin-left: 8px;
font-size: 11px;
color: rgba(232, 236, 244, 0.4);
}
</style>