mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-03-21 17:33:10 +00:00
添加全屏时间卡片功能,支持时钟、倒计时和秒表模式,优化用户交互体验
This commit is contained in:
parent
c4b95aede2
commit
f1838a891b
@ -5,9 +5,11 @@
|
|||||||
border
|
border
|
||||||
rounded="xl"
|
rounded="xl"
|
||||||
height="100%"
|
height="100%"
|
||||||
|
style="cursor: pointer"
|
||||||
|
@click="showFullscreen = true"
|
||||||
>
|
>
|
||||||
<v-card-text
|
<v-card-text
|
||||||
class="pa-6 d-flex flex-column "
|
class="pa-6 d-flex flex-column"
|
||||||
style="height: 100%"
|
style="height: 100%"
|
||||||
>
|
>
|
||||||
<!-- 时间显示 -->
|
<!-- 时间显示 -->
|
||||||
@ -30,12 +32,407 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 全屏时间弹框 -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="showFullscreen"
|
||||||
|
fullscreen
|
||||||
|
:scrim="false"
|
||||||
|
transition="dialog-bottom-transition"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
class="fullscreen-time-card d-flex flex-column"
|
||||||
|
@mousemove="showToolbar"
|
||||||
|
@touchstart="showToolbar"
|
||||||
|
>
|
||||||
|
<!-- 顶部分页导航 (自动隐藏) -->
|
||||||
|
<Transition name="toolbar-fade">
|
||||||
|
<div
|
||||||
|
v-show="toolbarVisible"
|
||||||
|
class="fullscreen-toolbar"
|
||||||
|
>
|
||||||
|
<v-tabs
|
||||||
|
v-model="fullscreenMode"
|
||||||
|
density="comfortable"
|
||||||
|
color="primary"
|
||||||
|
align-tabs="center"
|
||||||
|
class="fullscreen-tabs"
|
||||||
|
>
|
||||||
|
<v-tab value="clock">
|
||||||
|
<v-icon
|
||||||
|
start
|
||||||
|
icon="mdi-clock-outline"
|
||||||
|
/>
|
||||||
|
时钟
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="countdown">
|
||||||
|
<v-icon
|
||||||
|
start
|
||||||
|
icon="mdi-timer-sand"
|
||||||
|
/>
|
||||||
|
倒计时
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="stopwatch">
|
||||||
|
<v-icon
|
||||||
|
start
|
||||||
|
icon="mdi-timer-outline"
|
||||||
|
/>
|
||||||
|
秒表
|
||||||
|
</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- 主体内容区 -->
|
||||||
|
<div class="fullscreen-time-body flex-grow-1 d-flex flex-column align-center justify-center">
|
||||||
|
<v-tabs-window
|
||||||
|
v-model="fullscreenMode"
|
||||||
|
class="fullscreen-tabs-window"
|
||||||
|
>
|
||||||
|
<!-- ========= 时钟模式 ========= -->
|
||||||
|
<v-tabs-window-item value="clock">
|
||||||
|
<div class="d-flex flex-column align-center justify-center">
|
||||||
|
<div class="fullscreen-time-display">
|
||||||
|
{{ timeString }}<span class="fullscreen-seconds">{{ secondsString }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="fullscreen-date-line mt-6">
|
||||||
|
{{ dateString }} {{ weekdayString }} {{ periodOfDay }}
|
||||||
|
</div>
|
||||||
|
<div class="fullscreen-progress mt-10">
|
||||||
|
<div class="text-caption text-medium-emphasis mb-1">
|
||||||
|
今日已过 {{ dayProgressPercent }}%
|
||||||
|
</div>
|
||||||
|
<v-progress-linear
|
||||||
|
:model-value="dayProgressPercent"
|
||||||
|
color="primary"
|
||||||
|
height="6"
|
||||||
|
rounded
|
||||||
|
style="max-width: 400px; width: 80vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="fullscreen-extra mt-8 text-medium-emphasis d-flex ga-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-h6 font-weight-bold">
|
||||||
|
{{ dayOfYear }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption">
|
||||||
|
今年第几天
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-h6 font-weight-bold">
|
||||||
|
{{ weekOfYear }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption">
|
||||||
|
今年第几周
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-h6 font-weight-bold">
|
||||||
|
{{ daysLeftInYear }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption">
|
||||||
|
距离新年
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<!-- ========= 倒计时模式 ========= -->
|
||||||
|
<v-tabs-window-item value="countdown">
|
||||||
|
<div class="d-flex flex-column align-center justify-center">
|
||||||
|
<!-- 未开始:选择倒计时时间 -->
|
||||||
|
<template v-if="!countdownRunning && countdownRemaining <= 0">
|
||||||
|
<div class="countdown-setup d-flex align-center ga-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-chevron-up"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="countdownHours = Math.min(countdownHours + 1, 99)"
|
||||||
|
/>
|
||||||
|
<div class="countdown-digit">
|
||||||
|
{{ String(countdownHours).padStart(2, '0') }}
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-chevron-down"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="countdownHours = Math.max(countdownHours - 1, 0)"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-medium-emphasis">
|
||||||
|
时
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="countdown-sep">
|
||||||
|
:
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-chevron-up"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="countdownMinutes = Math.min(countdownMinutes + 1, 59)"
|
||||||
|
/>
|
||||||
|
<div class="countdown-digit">
|
||||||
|
{{ String(countdownMinutes).padStart(2, '0') }}
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-chevron-down"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="countdownMinutes = Math.max(countdownMinutes - 1, 0)"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-medium-emphasis">
|
||||||
|
分
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="countdown-sep">
|
||||||
|
:
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-chevron-up"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="countdownSeconds = Math.min(countdownSeconds + 1, 59)"
|
||||||
|
/>
|
||||||
|
<div class="countdown-digit">
|
||||||
|
{{ String(countdownSeconds).padStart(2, '0') }}
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-chevron-down"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="countdownSeconds = Math.max(countdownSeconds - 1, 0)"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-medium-emphasis">
|
||||||
|
秒
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 快捷按钮 -->
|
||||||
|
<div class="mt-8 d-flex ga-3 flex-wrap justify-center">
|
||||||
|
<v-btn
|
||||||
|
v-for="preset in countdownPresets"
|
||||||
|
:key="preset.label"
|
||||||
|
variant="tonal"
|
||||||
|
rounded="xl"
|
||||||
|
@click="applyCountdownPreset(preset)"
|
||||||
|
>
|
||||||
|
{{ preset.label }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
size="x-large"
|
||||||
|
rounded="xl"
|
||||||
|
:disabled="countdownTotalSetSeconds <= 0"
|
||||||
|
prepend-icon="mdi-play"
|
||||||
|
@click="startCountdown"
|
||||||
|
>
|
||||||
|
开始
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 运行中/暂停 -->
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="fullscreen-time-display"
|
||||||
|
:class="{ 'countdown-ended': countdownRemaining <= 0 && !countdownRunning }"
|
||||||
|
>
|
||||||
|
{{ countdownDisplay }}
|
||||||
|
</div>
|
||||||
|
<div class="fullscreen-date-line mt-4 text-medium-emphasis">
|
||||||
|
{{ countdownRunning ? '倒计时进行中' : (countdownRemaining <= 0 ? '时间到!' : '已暂停') }}
|
||||||
|
</div>
|
||||||
|
<!-- 进度 -->
|
||||||
|
<v-progress-linear
|
||||||
|
:model-value="countdownProgressPercent"
|
||||||
|
:color="countdownRemaining <= 0 ? 'error' : 'primary'"
|
||||||
|
class="mt-8"
|
||||||
|
height="6"
|
||||||
|
rounded
|
||||||
|
style="max-width: 400px; width: 80vw"
|
||||||
|
/>
|
||||||
|
<div class="mt-8 d-flex ga-3">
|
||||||
|
<v-btn
|
||||||
|
v-if="countdownRemaining > 0"
|
||||||
|
:icon="countdownRunning ? 'mdi-pause' : 'mdi-play'"
|
||||||
|
:color="countdownRunning ? 'warning' : 'primary'"
|
||||||
|
size="x-large"
|
||||||
|
variant="tonal"
|
||||||
|
@click="toggleCountdown"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-stop"
|
||||||
|
color="error"
|
||||||
|
size="x-large"
|
||||||
|
variant="tonal"
|
||||||
|
@click="resetCountdown"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<!-- ========= 秒表模式 ========= -->
|
||||||
|
<v-tabs-window-item value="stopwatch">
|
||||||
|
<div class="d-flex flex-column align-center justify-center">
|
||||||
|
<div class="fullscreen-time-display">
|
||||||
|
{{ stopwatchDisplay }}
|
||||||
|
</div>
|
||||||
|
<div class="fullscreen-date-line mt-4 text-medium-emphasis">
|
||||||
|
{{ stopwatchRunning ? '计时中' : (stopwatchElapsed > 0 ? '已暂停' : '秒表') }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 d-flex ga-3">
|
||||||
|
<v-btn
|
||||||
|
:icon="stopwatchRunning ? 'mdi-pause' : 'mdi-play'"
|
||||||
|
:color="stopwatchRunning ? 'warning' : 'primary'"
|
||||||
|
size="x-large"
|
||||||
|
variant="tonal"
|
||||||
|
@click="toggleStopwatch"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
v-if="stopwatchRunning"
|
||||||
|
icon="mdi-flag"
|
||||||
|
color="info"
|
||||||
|
size="x-large"
|
||||||
|
variant="tonal"
|
||||||
|
@click="addLap"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
v-if="!stopwatchRunning && stopwatchElapsed > 0"
|
||||||
|
icon="mdi-stop"
|
||||||
|
color="error"
|
||||||
|
size="x-large"
|
||||||
|
variant="tonal"
|
||||||
|
@click="resetStopwatch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 计次记录 -->
|
||||||
|
<v-slide-y-transition>
|
||||||
|
<div
|
||||||
|
v-if="laps.length > 0"
|
||||||
|
class="stopwatch-laps mt-6"
|
||||||
|
>
|
||||||
|
<v-table
|
||||||
|
density="compact"
|
||||||
|
class="stopwatch-laps-table"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
#
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
计次
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
总计
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(lap, idx) in laps"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
{{ laps.length - idx }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ formatMs(lap.split) }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ formatMs(lap.total) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</div>
|
||||||
|
</v-slide-y-transition>
|
||||||
|
</div>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
</v-tabs-window>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右下角按钮组 -->
|
||||||
|
<div class="fullscreen-actions">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-cog"
|
||||||
|
variant="text"
|
||||||
|
size="large"
|
||||||
|
@click.stop="showSettings = true"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
variant="text"
|
||||||
|
size="large"
|
||||||
|
class="ml-2"
|
||||||
|
@click="showFullscreen = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 设置弹框 -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="showSettings"
|
||||||
|
max-width="420"
|
||||||
|
:scrim="true"
|
||||||
|
>
|
||||||
|
<v-card rounded="xl">
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon
|
||||||
|
class="mr-2"
|
||||||
|
icon="mdi-cog"
|
||||||
|
/>
|
||||||
|
时间卡片设置
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon
|
||||||
|
class="mr-3"
|
||||||
|
icon="mdi-clock-outline"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>显示时间卡片</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>在首页显示时间卡片,刷新后生效。</v-list-item-subtitle>
|
||||||
|
<template #append>
|
||||||
|
<v-switch
|
||||||
|
:model-value="timeCardEnabled"
|
||||||
|
hide-details
|
||||||
|
density="comfortable"
|
||||||
|
@update:model-value="setTimeCardEnabled"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
@click="showSettings = false"
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { SettingsManager, watchSettings } from '@/utils/settings'
|
import { SettingsManager, watchSettings, getSetting, setSetting } from '@/utils/settings'
|
||||||
|
import { playSound, defaultSingleSound } from '@/utils/soundList'
|
||||||
|
|
||||||
// 时间字体大小比例(大屏场景)
|
// 时间字体大小比例(卡片场景)
|
||||||
const TIME_FONT_RATIO = 2.0
|
const TIME_FONT_RATIO = 2.0
|
||||||
const SECONDS_FONT_RATIO = 0.9
|
const SECONDS_FONT_RATIO = 0.9
|
||||||
const DATE_FONT_RATIO = 0.6
|
const DATE_FONT_RATIO = 0.6
|
||||||
@ -48,6 +445,39 @@ export default {
|
|||||||
timer: null,
|
timer: null,
|
||||||
unwatch: null,
|
unwatch: null,
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
|
showFullscreen: false,
|
||||||
|
showSettings: false,
|
||||||
|
timeCardEnabled: true,
|
||||||
|
// 全屏模式切换
|
||||||
|
fullscreenMode: 'clock',
|
||||||
|
// 工具栏自动隐藏
|
||||||
|
toolbarVisible: true,
|
||||||
|
toolbarTimer: null,
|
||||||
|
// 倒计时
|
||||||
|
countdownHours: 0,
|
||||||
|
countdownMinutes: 5,
|
||||||
|
countdownSeconds: 0,
|
||||||
|
countdownRunning: false,
|
||||||
|
countdownRemaining: 0, // 剩余毫秒
|
||||||
|
countdownTotal: 0, // 总毫秒
|
||||||
|
countdownTimer: null,
|
||||||
|
countdownLastTick: null,
|
||||||
|
countdownPresets: [
|
||||||
|
{ label: '1 分钟', h: 0, m: 1, s: 0 },
|
||||||
|
{ label: '3 分钟', h: 0, m: 3, s: 0 },
|
||||||
|
{ label: '5 分钟', h: 0, m: 5, s: 0 },
|
||||||
|
{ label: '10 分钟', h: 0, m: 10, s: 0 },
|
||||||
|
{ label: '15 分钟', h: 0, m: 15, s: 0 },
|
||||||
|
{ label: '30 分钟', h: 0, m: 30, s: 0 },
|
||||||
|
{ label: '1 小时', h: 1, m: 0, s: 0 },
|
||||||
|
],
|
||||||
|
// 秒表
|
||||||
|
stopwatchRunning: false,
|
||||||
|
stopwatchElapsed: 0, // 已走毫秒
|
||||||
|
stopwatchTimer: null,
|
||||||
|
stopwatchLastTick: null,
|
||||||
|
laps: [],
|
||||||
|
lastLapElapsed: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -80,6 +510,31 @@ export default {
|
|||||||
if (h < 22) return '晚上'
|
if (h < 22) return '晚上'
|
||||||
return '深夜'
|
return '深夜'
|
||||||
},
|
},
|
||||||
|
dayProgressPercent() {
|
||||||
|
const h = this.now.getHours()
|
||||||
|
const m = this.now.getMinutes()
|
||||||
|
const s = this.now.getSeconds()
|
||||||
|
const totalSeconds = h * 3600 + m * 60 + s
|
||||||
|
return ((totalSeconds / 86400) * 100).toFixed(1)
|
||||||
|
},
|
||||||
|
dayOfYear() {
|
||||||
|
const start = new Date(this.now.getFullYear(), 0, 0)
|
||||||
|
const diff = this.now - start
|
||||||
|
const oneDay = 1000 * 60 * 60 * 24
|
||||||
|
return Math.floor(diff / oneDay)
|
||||||
|
},
|
||||||
|
weekOfYear() {
|
||||||
|
const d = new Date(Date.UTC(this.now.getFullYear(), this.now.getMonth(), this.now.getDate()))
|
||||||
|
const dayNum = d.getUTCDay() || 7
|
||||||
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum)
|
||||||
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
||||||
|
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7)
|
||||||
|
},
|
||||||
|
daysLeftInYear() {
|
||||||
|
const endOfYear = new Date(this.now.getFullYear(), 11, 31)
|
||||||
|
const diff = endOfYear - this.now
|
||||||
|
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||||
|
},
|
||||||
timeStyle() {
|
timeStyle() {
|
||||||
return {
|
return {
|
||||||
'font-size': `${this.fontSize * TIME_FONT_RATIO}px`,
|
'font-size': `${this.fontSize * TIME_FONT_RATIO}px`,
|
||||||
@ -104,6 +559,62 @@ export default {
|
|||||||
'letter-spacing': '1px',
|
'letter-spacing': '1px',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 倒计时 computed
|
||||||
|
countdownTotalSetSeconds() {
|
||||||
|
return this.countdownHours * 3600 + this.countdownMinutes * 60 + this.countdownSeconds
|
||||||
|
},
|
||||||
|
countdownDisplay() {
|
||||||
|
const totalSec = Math.max(0, Math.ceil(this.countdownRemaining / 1000))
|
||||||
|
const h = Math.floor(totalSec / 3600)
|
||||||
|
const m = Math.floor((totalSec % 3600) / 60)
|
||||||
|
const s = totalSec % 60
|
||||||
|
if (h > 0) {
|
||||||
|
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||||||
|
},
|
||||||
|
countdownProgressPercent() {
|
||||||
|
if (this.countdownTotal <= 0) return 0
|
||||||
|
return ((this.countdownTotal - this.countdownRemaining) / this.countdownTotal) * 100
|
||||||
|
},
|
||||||
|
// 秒表 computed
|
||||||
|
stopwatchDisplay() {
|
||||||
|
const ms = this.stopwatchElapsed
|
||||||
|
const totalSec = Math.floor(ms / 1000)
|
||||||
|
const h = Math.floor(totalSec / 3600)
|
||||||
|
const m = Math.floor((totalSec % 3600) / 60)
|
||||||
|
const s = totalSec % 60
|
||||||
|
const centis = Math.floor((ms % 1000) / 10)
|
||||||
|
if (h > 0) {
|
||||||
|
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(centis).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(centis).padStart(2, '0')}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
showFullscreen(val) {
|
||||||
|
if (val) {
|
||||||
|
this.handleKeydown = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (this.showSettings) {
|
||||||
|
this.showSettings = false
|
||||||
|
} else {
|
||||||
|
this.showFullscreen = false
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', this.handleKeydown, true)
|
||||||
|
this.showToolbar()
|
||||||
|
} else {
|
||||||
|
if (this.handleKeydown) {
|
||||||
|
window.removeEventListener('keydown', this.handleKeydown, true)
|
||||||
|
this.handleKeydown = null
|
||||||
|
}
|
||||||
|
this.clearToolbarTimer()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadSettings()
|
this.loadSettings()
|
||||||
@ -114,16 +625,26 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.stopTimer()
|
this.stopTimer()
|
||||||
|
this.clearCountdownTimer()
|
||||||
|
this.clearStopwatchTimer()
|
||||||
|
this.clearToolbarTimer()
|
||||||
if (this.unwatch) {
|
if (this.unwatch) {
|
||||||
this.unwatch()
|
this.unwatch()
|
||||||
}
|
}
|
||||||
|
if (this.handleKeydown) {
|
||||||
|
window.removeEventListener('keydown', this.handleKeydown, true)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadSettings() {
|
loadSettings() {
|
||||||
this.fontSize = SettingsManager.getSetting('font.size')
|
this.fontSize = SettingsManager.getSetting('font.size')
|
||||||
|
this.timeCardEnabled = getSetting('timeCard.enabled')
|
||||||
|
},
|
||||||
|
setTimeCardEnabled(val) {
|
||||||
|
this.timeCardEnabled = val
|
||||||
|
setSetting('timeCard.enabled', val)
|
||||||
},
|
},
|
||||||
startTimer() {
|
startTimer() {
|
||||||
// 每秒更新一次
|
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
this.now = new Date()
|
this.now = new Date()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@ -134,6 +655,118 @@ export default {
|
|||||||
this.timer = null
|
this.timer = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---- 工具栏自动隐藏 ----
|
||||||
|
showToolbar() {
|
||||||
|
this.toolbarVisible = true
|
||||||
|
this.clearToolbarTimer()
|
||||||
|
this.toolbarTimer = setTimeout(() => {
|
||||||
|
this.toolbarVisible = false
|
||||||
|
}, 3000)
|
||||||
|
},
|
||||||
|
clearToolbarTimer() {
|
||||||
|
if (this.toolbarTimer) {
|
||||||
|
clearTimeout(this.toolbarTimer)
|
||||||
|
this.toolbarTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- 倒计时 ----
|
||||||
|
applyCountdownPreset(preset) {
|
||||||
|
this.countdownHours = preset.h
|
||||||
|
this.countdownMinutes = preset.m
|
||||||
|
this.countdownSeconds = preset.s
|
||||||
|
},
|
||||||
|
startCountdown() {
|
||||||
|
const totalMs = this.countdownTotalSetSeconds * 1000
|
||||||
|
if (totalMs <= 0) return
|
||||||
|
this.countdownTotal = totalMs
|
||||||
|
this.countdownRemaining = totalMs
|
||||||
|
this.countdownRunning = true
|
||||||
|
this.countdownLastTick = Date.now()
|
||||||
|
this.countdownTimer = setInterval(() => {
|
||||||
|
this.tickCountdown()
|
||||||
|
}, 50)
|
||||||
|
},
|
||||||
|
tickCountdown() {
|
||||||
|
const now = Date.now()
|
||||||
|
const delta = now - this.countdownLastTick
|
||||||
|
this.countdownLastTick = now
|
||||||
|
this.countdownRemaining = Math.max(0, this.countdownRemaining - delta)
|
||||||
|
if (this.countdownRemaining <= 0) {
|
||||||
|
this.countdownRunning = false
|
||||||
|
this.clearCountdownTimer()
|
||||||
|
playSound(defaultSingleSound)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleCountdown() {
|
||||||
|
if (this.countdownRunning) {
|
||||||
|
this.countdownRunning = false
|
||||||
|
this.clearCountdownTimer()
|
||||||
|
} else {
|
||||||
|
this.countdownRunning = true
|
||||||
|
this.countdownLastTick = Date.now()
|
||||||
|
this.countdownTimer = setInterval(() => {
|
||||||
|
this.tickCountdown()
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetCountdown() {
|
||||||
|
this.countdownRunning = false
|
||||||
|
this.countdownRemaining = 0
|
||||||
|
this.countdownTotal = 0
|
||||||
|
this.clearCountdownTimer()
|
||||||
|
},
|
||||||
|
clearCountdownTimer() {
|
||||||
|
if (this.countdownTimer) {
|
||||||
|
clearInterval(this.countdownTimer)
|
||||||
|
this.countdownTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- 秒表 ----
|
||||||
|
toggleStopwatch() {
|
||||||
|
if (this.stopwatchRunning) {
|
||||||
|
this.stopwatchRunning = false
|
||||||
|
this.clearStopwatchTimer()
|
||||||
|
} else {
|
||||||
|
this.stopwatchRunning = true
|
||||||
|
this.stopwatchLastTick = Date.now()
|
||||||
|
this.stopwatchTimer = setInterval(() => {
|
||||||
|
this.tickStopwatch()
|
||||||
|
}, 30)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tickStopwatch() {
|
||||||
|
const now = Date.now()
|
||||||
|
this.stopwatchElapsed += now - this.stopwatchLastTick
|
||||||
|
this.stopwatchLastTick = now
|
||||||
|
},
|
||||||
|
addLap() {
|
||||||
|
const split = this.stopwatchElapsed - this.lastLapElapsed
|
||||||
|
this.laps.unshift({ split, total: this.stopwatchElapsed })
|
||||||
|
this.lastLapElapsed = this.stopwatchElapsed
|
||||||
|
},
|
||||||
|
resetStopwatch() {
|
||||||
|
this.stopwatchRunning = false
|
||||||
|
this.stopwatchElapsed = 0
|
||||||
|
this.lastLapElapsed = 0
|
||||||
|
this.laps = []
|
||||||
|
this.clearStopwatchTimer()
|
||||||
|
},
|
||||||
|
clearStopwatchTimer() {
|
||||||
|
if (this.stopwatchTimer) {
|
||||||
|
clearInterval(this.stopwatchTimer)
|
||||||
|
this.stopwatchTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatMs(ms) {
|
||||||
|
const totalSec = Math.floor(ms / 1000)
|
||||||
|
const m = Math.floor(totalSec / 60)
|
||||||
|
const s = totalSec % 60
|
||||||
|
const centis = Math.floor((ms % 1000) / 10)
|
||||||
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(centis).padStart(2, '0')}`
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -161,4 +794,147 @@ export default {
|
|||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 全屏样式 */
|
||||||
|
.fullscreen-time-card {
|
||||||
|
position: relative;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部工具栏 */
|
||||||
|
.fullscreen-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-tabs {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-fade-enter-active,
|
||||||
|
.toolbar-fade-leave-active {
|
||||||
|
transition: opacity 0.4s ease, transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-fade-enter-from,
|
||||||
|
.toolbar-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-tabs-window {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-time-body {
|
||||||
|
user-select: none;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-time-display {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||||
|
font-size: clamp(4rem, 15vw, 12rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 8px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-seconds {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||||
|
font-size: 0.45em;
|
||||||
|
vertical-align: baseline;
|
||||||
|
margin-left: 4px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-date-line {
|
||||||
|
font-size: clamp(1rem, 3vw, 2.2rem);
|
||||||
|
opacity: 0.7;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-extra {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-actions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-actions:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 倒计时设置 */
|
||||||
|
.countdown-setup {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-digit {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||||
|
font-size: clamp(3rem, 10vw, 8rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 1.2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-sep {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||||
|
font-size: clamp(3rem, 10vw, 8rem);
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.4;
|
||||||
|
padding-bottom: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-ended {
|
||||||
|
animation: pulse-red 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-red {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 秒表计次列表 */
|
||||||
|
.stopwatch-laps {
|
||||||
|
max-height: 30vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: min(90vw, 400px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stopwatch-laps-table {
|
||||||
|
background: transparent !important;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user