Files
vela-tomato-clock/src/pages/5/timer/timer.ux
T

364 lines
9.0 KiB
XML
Raw Normal View History

2026-08-17 09:17:10 +08:00
<template>
<swiper class="timer-swiper" vertical="true" indicator="false" loop="false">
<div class="timer-page">
<text class="page-title">{{isResting ? "休息中" : "专注中"}}</text>
<text class="clock-label">{{ time }}</text>
<text class="countdown">{{ countdown }}</text>
<text class="focus-label">{{isResting ? "休息中" : "专注中"}}</text>
<div class="stop-button" onclick="stopTimer">
<image class="button-background" src="/common/icons/button_grey.png"></image>
<image class="stop-icon" src="/common/icons/stop.png"></image>
</div>
<div class="pause-button" onclick="togglePause">
<image class="button-background" src="{{isResting ? '/common/icons/button_yellow.png' : '/common/icons/button2.png'}}"></image>
<image class="pause-icon" src="{{paused ? '/common/icons/MdiPlay.png' : '/common/icons/pause.png'}}"></image>
</div>
</div>
<div class="stats-page">
<text class="page-title">专注统计</text>
<text class="clock-label">{{ time }}</text>
<text class="stats-title focus-title">累计专注</text>
<text class="stats-value focus-value">{{ focusTotalText }}</text>
<text class="stats-title rest-title">累计休息</text>
<text class="stats-value rest-value">{{ restTotalText }}</text>
</div>
</swiper>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
import vibrator from "@system.vibrator"
export default {
protected: { duration: "m:20" },
private: {
time: "00:00",
countdown: "20:00",
clockTimer: null,
countdownTimer: null,
remaining: 1200,
paused: false,
isResting: false,
focusDurationSeconds: 1200,
2026-08-29 18:13:48 +08:00
restDurationSeconds: 300,
// 息屏后 setInterval 会被系统挂起,所有计时以时间戳为准
phaseEndAt: 0,
lastCountedAt: 0,
lastPersistAt: 0,
focusTotalMs: 0,
restTotalMs: 0,
2026-08-17 09:17:10 +08:00
focusTotalText: "00:00:00",
restTotalText: "00:00:00"
},
onInit() {
this.loadStats()
this.startCountdown(this.durationSeconds(this.duration))
this.updateTime()
this.clockTimer = setInterval(() => this.updateTime(), 1000)
},
2026-08-29 18:13:48 +08:00
onShow() {
// 亮屏后立刻按时间戳补齐息屏期间的进度
this.updateTime()
this.sync()
},
2026-08-17 09:17:10 +08:00
onDestroy() {
clearInterval(this.clockTimer)
clearInterval(this.countdownTimer)
2026-08-29 18:13:48 +08:00
this.countdownTimer = null
if (!this.paused) {
this.accumulate(Date.now())
this.updateStats()
}
this.persistStats(true)
2026-08-17 09:17:10 +08:00
},
startCountdown(seconds) {
this.focusDurationSeconds = seconds > 0 ? seconds : 1200
2026-08-29 18:13:48 +08:00
this.beginPhase(false, this.focusDurationSeconds, Date.now())
this.ensureTicking()
2026-08-17 09:17:10 +08:00
},
2026-08-29 18:13:48 +08:00
startRest(startAt) {
this.beginPhase(true, this.restDurationSeconds, startAt)
2026-08-17 09:17:10 +08:00
this.paused = true
2026-08-29 18:13:48 +08:00
},
beginPhase(isResting, durationSeconds, startAt) {
this.isResting = isResting
this.paused = false
this.phaseEndAt = startAt + durationSeconds * 1000
this.lastCountedAt = startAt
this.remaining = durationSeconds
2026-08-17 09:17:10 +08:00
this.updateCountdown()
},
2026-08-29 18:13:48 +08:00
ensureTicking() {
if (this.countdownTimer === null) {
this.countdownTimer = setInterval(() => this.sync(), 1000)
}
},
2026-08-17 09:17:10 +08:00
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
2026-08-29 18:13:48 +08:00
// lastCountedAt untilAt 之间的时间计入当前阶段的累计值
accumulate(untilAt) {
const delta = untilAt - this.lastCountedAt
if (delta <= 0) return
if (this.isResting) this.restTotalMs += delta
else this.focusTotalMs += delta
this.lastCountedAt = untilAt
},
sync() {
// phaseEndAt 0 表示还没开始计时
if (this.paused || this.phaseEndAt === 0) return
const now = Date.now()
// 息屏期间可能已跨过一个或多个阶段,逐段结算
while (now >= this.phaseEndAt) {
this.accumulate(this.phaseEndAt)
const endedAt = this.phaseEndAt
2026-08-17 09:17:10 +08:00
if (this.isResting) {
vibrator.vibrate({ mode: "short" })
2026-08-29 18:13:48 +08:00
this.beginPhase(false, this.focusDurationSeconds, endedAt)
2026-08-17 09:17:10 +08:00
} else {
vibrator.vibrate({ mode: "long" })
2026-08-29 18:13:48 +08:00
this.startRest(endedAt)
break
2026-08-17 09:17:10 +08:00
}
}
2026-08-29 18:13:48 +08:00
if (!this.paused) {
this.accumulate(now)
this.remaining = Math.max(0, Math.round((this.phaseEndAt - now) / 1000))
this.updateCountdown()
}
this.updateStats()
this.persistStats(false)
2026-08-17 09:17:10 +08:00
},
durationSeconds(config) {
if (config && config.indexOf("s:") === 0) return parseInt(config.substring(2), 10) || 5
if (config && config.indexOf("m:") === 0) return (parseInt(config.substring(2), 10) || 20) * 60
return (parseInt(config, 10) || 20) * 60
},
updateCountdown() {
const minutes = Math.floor(this.remaining / 60)
const seconds = this.remaining % 60
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.countdown = `${pad(minutes)}:${pad(seconds)}`
},
updateStats() {
2026-08-29 18:13:48 +08:00
this.focusTotalText = this.formatDuration(Math.floor(this.focusTotalMs / 1000))
this.restTotalText = this.formatDuration(Math.floor(this.restTotalMs / 1000))
2026-08-17 09:17:10 +08:00
},
formatDuration(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
},
loadStats() {
file.readText({
uri: "internal://files/focus-stats.json",
success: (data) => {
try {
const stats = JSON.parse(data.text)
if (stats.date === this.today()) {
2026-08-29 18:13:48 +08:00
// 读取是异步的,期间可能已经累计了一点时间,用加法避免被覆盖
this.focusTotalMs += (parseInt(stats.focus, 10) || 0) * 1000
this.restTotalMs += (parseInt(stats.rest, 10) || 0) * 1000
2026-08-17 09:17:10 +08:00
}
} catch (error) {}
this.updateStats()
},
fail: () => this.updateStats()
})
},
2026-08-29 18:13:48 +08:00
persistStats(force) {
const now = Date.now()
// 每秒写盘没有必要,节流到 5 秒;退出/暂停时强制落盘
if (!force && now - this.lastPersistAt < 5000) return
this.lastPersistAt = now
2026-08-17 09:17:10 +08:00
file.writeText({
uri: "internal://files/focus-stats.json",
text: JSON.stringify({
date: this.today(),
2026-08-29 18:13:48 +08:00
focus: Math.floor(this.focusTotalMs / 1000),
rest: Math.floor(this.restTotalMs / 1000)
2026-08-17 09:17:10 +08:00
})
})
},
today() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
},
togglePause() {
2026-08-29 18:13:48 +08:00
if (this.paused) {
// 恢复:把剩余秒数换算成新的结束时间戳
const now = Date.now()
this.phaseEndAt = now + this.remaining * 1000
this.lastCountedAt = now
this.paused = false
this.ensureTicking()
} else {
this.accumulate(Date.now())
this.paused = true
this.updateStats()
this.persistStats(true)
}
2026-08-17 09:17:10 +08:00
},
stopTimer() {
router.back()
}
}
</script>
<style>
.timer-swiper,
.timer-page,
.stats-page {
width: 432px;
height: 514px;
background-color: #000000;
}
.timer-page,
.stats-page {
position: relative;
}
.page-title {
position: absolute;
left: 33px;
top: 19px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: left;
}
.clock-label {
position: absolute;
left: 276px;
top: 19px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 30px;
font-weight: 500;
line-height: 33.6px;
text-align: right;
}
.countdown {
position: absolute;
left: 66px;
top: 189px;
width: 300px;
height: 97px;
color: #ffffff;
font-size: 64px;
font-weight: 500;
line-height: 84.86px;
text-align: center;
}
.focus-label {
position: absolute;
left: 66px;
top: 287px;
width: 300px;
height: 38px;
color: rgba(255, 255, 255, 0.6);
font-size: 32px;
font-weight: 500;
line-height: 42.43px;
text-align: center;
}
.stop-button,
.pause-button {
position: absolute;
top: 435px;
width: 140px;
height: 69px;
}
.stop-button { left: 66px; }
.pause-button { left: 226px; }
.button-background {
position: absolute;
left: 0px;
top: 0px;
width: 140px;
height: 69px;
}
.stop-icon {
position: absolute;
left: 59px;
top: 23px;
width: 22px;
height: 24px;
}
.pause-icon {
position: absolute;
left: 53px;
top: 16px;
width: 35px;
height: 38px;
}
.stats-title,
.stats-value {
position: absolute;
left: 66px;
width: 300px;
text-align: center;
}
.stats-title {
height: 43px;
font-size: 32px;
font-weight: 500;
line-height: 42.43px;
}
.stats-value {
height: 50px;
color: #ffffff;
font-size: 40px;
font-weight: 500;
line-height: 53.04px;
}
.focus-title { top: 161px; color: rgba(112, 119, 255, 1); }
.focus-value { top: 204px; }
.rest-title { top: 260px; color: rgba(149, 168, 0, 1); }
.rest-value { top: 303px; }
</style>