基于Vue3的打卡計(jì)時(shí)器實(shí)現(xiàn)方案
引言
在日常工作和生活中,我們經(jīng)常需要記錄時(shí)間間隔,比如工作打卡、學(xué)習(xí)時(shí)長(zhǎng)、項(xiàng)目耗時(shí)等。今天我將為大家介紹一個(gè)基于Vue3的打卡計(jì)時(shí)器實(shí)現(xiàn)方案,它具有以下特點(diǎn):
- ? 從打卡開(kāi)始自動(dòng)計(jì)時(shí)
- ? 支持暫停/繼續(xù)功能
- ? 可記錄多個(gè)時(shí)間點(diǎn)(不重置計(jì)時(shí))
- ? 清晰的視覺(jué)反饋
- ? 響應(yīng)式設(shè)計(jì),適配各種設(shè)備
功能概述
本計(jì)時(shí)器主要實(shí)現(xiàn)以下核心功能:
- 持續(xù)計(jì)時(shí):從開(kāi)始打卡起持續(xù)計(jì)時(shí),顯示當(dāng)前已用時(shí)間
- 暫停/繼續(xù):可隨時(shí)暫停計(jì)時(shí),再次點(diǎn)擊繼續(xù)
- 記錄時(shí)間點(diǎn):在計(jì)時(shí)過(guò)程中記錄關(guān)鍵時(shí)間點(diǎn),不重置計(jì)時(shí)器
- 重置功能:一鍵重置所有狀態(tài)和記錄
- 時(shí)間顯示:以時(shí):分:秒格式顯示時(shí)間
代碼實(shí)現(xiàn)
1. 完整HTML文件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>打卡計(jì)時(shí)器</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.header {
background: #2c3e50;
color: white;
padding: 25px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header p {
opacity: 0.8;
font-size: 16px;
}
.content {
padding: 30px;
}
.timer-display {
background: #f8f9fa;
border-radius: 15px;
padding: 25px;
text-align: center;
margin-bottom: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
}
.time {
font-size: 48px;
font-weight: bold;
color: #2c3e50;
margin: 10px 0;
font-family: 'Courier New', monospace;
letter-spacing: 2px;
}
.status {
font-size: 16px;
color: #7f8c8d;
margin-top: 5px;
}
.controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 30px;
}
.btn {
padding: 15px;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
color: white;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.btn:active {
transform: translateY(0);
}
.btn-start {
background: #27ae60;
}
.btn-pause {
background: #e67e22;
}
.btn-record {
background: #3498db;
}
.btn-reset {
background: #e74c3c;
}
.btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
transform: none;
}
.records {
background: #f8f9fa;
border-radius: 15px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
}
.records h2 {
color: #2c3e50;
margin-bottom: 15px;
font-size: 22px;
display: flex;
align-items: center;
}
.records h2 i {
margin-right: 10px;
}
.record-list {
max-height: 200px;
overflow-y: auto;
}
.record-item {
display: flex;
justify-content: space-between;
padding: 12px 15px;
border-bottom: 1px solid #eee;
animation: fadeIn 0.3s ease;
}
.record-item:last-child {
border-bottom: none;
}
.record-time {
font-weight: 600;
color: #3498db;
}
.record-duration {
color: #7f8c8d;
font-size: 14px;
}
.empty-records {
text-align: center;
padding: 20px;
color: #7f8c8d;
font-style: italic;
}
.footer {
text-align: center;
padding: 20px;
color: #7f8c8d;
font-size: 14px;
border-top: 1px solid #eee;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 500px) {
.controls {
grid-template-columns: 1fr;
}
.time {
font-size: 36px;
}
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<h1>打卡計(jì)時(shí)器</h1>
<p>記錄您從打卡開(kāi)始到結(jié)束的時(shí)間</p>
</div>
<div class="content">
<div class="timer-display">
<div>計(jì)時(shí)器</div>
<div class="time">{{ formattedTime }}</div>
<div class="status">{{ timerStatus }}</div>
</div>
<div class="controls">
<button class="btn btn-start" @click="startTimer" :disabled="isRunning">
開(kāi)始計(jì)時(shí)
</button>
<button class="btn btn-pause" @click="pauseTimer" :disabled="!isRunning">
暫停計(jì)時(shí)
</button>
<button class="btn btn-record" @click="recordTime" :disabled="!isRunning && elapsed === 0">
記錄時(shí)間
</button>
<button class="btn btn-reset" @click="resetTimer" class="grid-column: 1 / -1;">
重置計(jì)時(shí)器
</button>
</div>
<div class="records">
<h2>?? 時(shí)間記錄</h2>
<div class="record-list">
<div v-if="records.length === 0" class="empty-records">
暫無(wú)記錄,開(kāi)始計(jì)時(shí)后點(diǎn)擊"記錄時(shí)間"按鈕
</div>
<div v-for="(record, index) in records" :key="index" class="record-item">
<span class="record-time">{{ record.time }}</span>
<span class="record-duration">持續(xù): {{ formatDuration(record.elapsed) }}</span>
</div>
</div>
</div>
</div>
<div class="footer">
<p>提示:開(kāi)始計(jì)時(shí)后,您可以多次記錄時(shí)間點(diǎn),計(jì)時(shí)器不會(huì)重置</p>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onUnmounted } = Vue
createApp({
setup() {
// 響應(yīng)式狀態(tài)
const isRunning = ref(false)
const elapsed = ref(0) // 已經(jīng)過(guò)去的時(shí)間(毫秒)
const startTime = ref(null)
const timer = ref(null)
const records = ref([])
// 計(jì)時(shí)器狀態(tài)文本
const timerStatus = computed(() => {
if (isRunning.value) return '計(jì)時(shí)中...'
if (elapsed.value > 0) return '已暫停'
return '準(zhǔn)備就緒'
})
// 格式化時(shí)間顯示(時(shí):分:秒)
const formattedTime = computed(() => {
return formatDuration(elapsed.value)
})
// 格式化持續(xù)時(shí)間
function formatDuration(ms) {
const totalSeconds = Math.floor(ms / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
// 開(kāi)始計(jì)時(shí)
function startTimer() {
if (isRunning.value) return
isRunning.value = true
startTime.value = Date.now() - elapsed.value
timer.value = setInterval(() => {
elapsed.value = Date.now() - startTime.value
}, 100) // 每100毫秒更新一次,提高流暢度
}
// 暫停計(jì)時(shí)
function pauseTimer() {
if (!isRunning.value) return
isRunning.value = false
clearInterval(timer.value)
timer.value = null
}
// 記錄當(dāng)前時(shí)間
function recordTime() {
if (elapsed.value === 0) return
const now = new Date()
const timeString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
records.value.push({
time: timeString,
elapsed: elapsed.value
})
}
// 重置計(jì)時(shí)器
function resetTimer() {
pauseTimer()
elapsed.value = 0
records.value = []
startTime.value = null
}
// 組件卸載時(shí)清理定時(shí)器
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
})
return {
isRunning,
elapsed,
records,
timerStatus,
formattedTime,
startTimer,
pauseTimer,
recordTime,
resetTimer,
formatDuration
}
}
}).mount('#app')
</script>
</body>
</html>2. 核心狀態(tài)管理
// 響應(yīng)式狀態(tài) const isRunning = ref(false) // 計(jì)時(shí)器是否正在運(yùn)行 const elapsed = ref(0) // 已經(jīng)過(guò)去的時(shí)間(毫秒) const startTime = ref(null) // 計(jì)時(shí)開(kāi)始時(shí)間戳 const timer = ref(null) // 定時(shí)器引用 const records = ref([]) // 記錄的時(shí)間點(diǎn)列表
3. 核心功能函數(shù)
開(kāi)始計(jì)時(shí):
function startTimer() {
if (isRunning.value) return
isRunning.value = true
startTime.value = Date.now() - elapsed.value
timer.value = setInterval(() => {
elapsed.value = Date.now() - startTime.value
}, 100) // 每100毫秒更新一次,提高流暢度
}暫停計(jì)時(shí):
function pauseTimer() {
if (!isRunning.value) return
isRunning.value = false
clearInterval(timer.value)
timer.value = null
}記錄時(shí)間點(diǎn):
unction recordTime() {
if (elapsed.value === 0) return
const now = new Date()
const timeString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
records.value.push({
time: timeString,
elapsed: elapsed.value
})
}4. 時(shí)間格式化函數(shù)
function formatDuration(ms) {
const totalSeconds = Math.floor(ms / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
功能演示
使用流程
- 1.開(kāi)始打卡:點(diǎn)擊"開(kāi)始計(jì)時(shí)"按鈕啟動(dòng)計(jì)時(shí)器
- 2.持續(xù)計(jì)時(shí):計(jì)時(shí)器自動(dòng)計(jì)算并顯示從開(kāi)始到現(xiàn)在的時(shí)間
- 3.記錄關(guān)鍵時(shí)間點(diǎn):點(diǎn)擊"記錄時(shí)間"按鈕保存當(dāng)前時(shí)間(計(jì)時(shí)器繼續(xù)運(yùn)行)
- 4.暫停/繼續(xù):可以隨時(shí)暫停計(jì)時(shí),再次點(diǎn)擊繼續(xù)
- 5.重置:點(diǎn)擊"重置計(jì)時(shí)器"清除所有記錄和計(jì)時(shí)狀態(tài)
界面預(yù)覽

使用場(chǎng)景
這個(gè)計(jì)時(shí)器適用于多種場(chǎng)景:
- 1.工作打卡:記錄上班、下班時(shí)間,計(jì)算工作時(shí)長(zhǎng)
- 2.學(xué)習(xí)計(jì)時(shí):記錄學(xué)習(xí)開(kāi)始和結(jié)束時(shí)間,統(tǒng)計(jì)學(xué)習(xí)時(shí)長(zhǎng)
- 3.項(xiàng)目管理:記錄任務(wù)開(kāi)始時(shí)間,監(jiān)控項(xiàng)目進(jìn)度
- 4.運(yùn)動(dòng)健身:記錄運(yùn)動(dòng)開(kāi)始時(shí)間,監(jiān)控運(yùn)動(dòng)時(shí)長(zhǎng)
- 5.會(huì)議記錄:記錄會(huì)議開(kāi)始和關(guān)鍵節(jié)點(diǎn)時(shí)間
優(yōu)化建議
1. 本地存儲(chǔ)支持
可以添加localStorage支持,刷新頁(yè)面后保留計(jì)時(shí)狀態(tài):
// 保存到本地存儲(chǔ)
function saveToStorage() {
localStorage.setItem('timerState', JSON.stringify({
elapsed: elapsed.value,
records: records.value,
isRunning: isRunning.value
}))
}
// 從本地存儲(chǔ)加載
function loadFromStorage() {
const saved = localStorage.getItem('timerState')
if (saved) {
const state = JSON.parse(saved)
elapsed.value = state.elapsed || 0
records.value = state.records || []
isRunning.value = false // 默認(rèn)不自動(dòng)運(yùn)行
}
}2. 導(dǎo)出功能
添加導(dǎo)出記錄功能:
unction exportRecords() {
const data = records.value.map(r =>
`時(shí)間點(diǎn): ${r.time}, 持續(xù)時(shí)間: ${formatDuration(r.elapsed)}`
).join('\n')
const blob = new Blob([data], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '計(jì)時(shí)記錄.txt'
a.click()
URL.revokeObjectURL(url)
}3. 響應(yīng)式優(yōu)化
對(duì)于移動(dòng)端,可以添加觸摸事件支持:
// 在移動(dòng)端添加觸摸反饋
function addTouchFeedback() {
const buttons = document.querySelectorAll('.btn')
buttons.forEach(btn => {
btn.addEventListener('touchstart', function() {
this.style.transform = 'scale(0.95)'
})
btn.addEventListener('touchend', function() {
this.style.transform = ''
})
})
}總結(jié)
本文介紹了基于Vue3的打卡計(jì)時(shí)器完整實(shí)現(xiàn),涵蓋了:
- ? 核心功能實(shí)現(xiàn)(計(jì)時(shí)、暫停、記錄、重置)
- ? 代碼結(jié)構(gòu)和關(guān)鍵函數(shù)解析
- ? 使用流程和界面預(yù)覽
- ? 適用場(chǎng)景分析
- ? 優(yōu)化建議(本地存儲(chǔ)、導(dǎo)出功能、響應(yīng)式優(yōu)化)
這個(gè)計(jì)時(shí)器組件可以直接集成到任何Vue3項(xiàng)目中,也可以作為獨(dú)立頁(yè)面使用。代碼簡(jiǎn)潔明了,易于理解和擴(kuò)展。
希望這個(gè)計(jì)時(shí)器能幫助您更好地管理時(shí)間,提高工作效率!如果您有任何問(wèn)題或建議,歡迎在評(píng)論區(qū)留言交流。
以上就是基于Vue3的打卡計(jì)時(shí)器實(shí)現(xiàn)方案的詳細(xì)內(nèi)容,更多關(guān)于Vue3打卡計(jì)時(shí)器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue實(shí)現(xiàn)一個(gè)滾動(dòng)條樣式
滾動(dòng)條能夠給用戶帶來(lái)極好的體驗(yàn)效果,今天通過(guò)本文給大家分享vue實(shí)現(xiàn)一個(gè)滾動(dòng)條樣式,代碼簡(jiǎn)單易懂,需要的朋友參考下吧2021-07-07
vue前端el-input輸入限制輸入位數(shù)及輸入規(guī)則
這篇文章主要給大家介紹了關(guān)于vue前端el-input輸入限制輸入位數(shù)及輸入規(guī)則的相關(guān)資料,文中通過(guò)代碼介紹的介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用vue具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09
Vue綁定對(duì)象與數(shù)組變量更改后無(wú)法渲染問(wèn)題解決
這篇文章主要介紹了Vue綁定對(duì)象與數(shù)組變量更改后無(wú)法渲染問(wèn)題解決,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09
Vue腳手架學(xué)習(xí)之項(xiàng)目創(chuàng)建方式
這篇文章主要給大家介紹了關(guān)于Vue腳手架學(xué)習(xí)之項(xiàng)目創(chuàng)建方式的相關(guān)資料,文中通過(guò)介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
vue2.x中monaco-editor的使用說(shuō)明
這篇文章主要介紹了vue2.x中monaco-editor的使用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
vue3使用xgPalyer實(shí)現(xiàn)截圖功能的方法詳解
這篇文章主要為大家詳細(xì)介紹了如何在vue3中使用xgPalyer截圖功能,以及自定義插件,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02
uni-app 使用編輯器創(chuàng)建vue3 項(xiàng)目并且運(yùn)行的操作方法
這篇文章主要介紹了uni-app 使用編輯器創(chuàng)建vue3 項(xiàng)目并且運(yùn)行的操作方法,目前uniapp 創(chuàng)建的vue3支持 vue3.0 -- 3.2版本 也就是說(shuō)setup語(yǔ)法糖也是支持的,需要的朋友可以參考下2023-01-01

