feat(工具箱): 添加“计时器”工具
This commit is contained in:
@@ -301,6 +301,17 @@ export const toolList = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
changelogs: CHANGE_LOGS['keep-screen-on'],
|
changelogs: CHANGE_LOGS['keep-screen-on'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'timer-tool',
|
||||||
|
component: 'Other/TimerTool',
|
||||||
|
title: '计时器',
|
||||||
|
iconClass: 'mdi mdi-timer-outline',
|
||||||
|
desc: '正计时、倒计时工具。',
|
||||||
|
createdAt: '2025-12-29',
|
||||||
|
updatedAt: '2025-12-29',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'open-new-window',
|
id: 'open-new-window',
|
||||||
component: 'Other/OpenNewWindow',
|
component: 'Other/OpenNewWindow',
|
||||||
|
|||||||
416
src/views/ToolboxView/Other/TimerTool.vue
Normal file
416
src/views/ToolboxView/Other/TimerTool.vue
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page">
|
||||||
|
|
||||||
|
<!-- 计时器显示 -->
|
||||||
|
<n-card size="small" title="计时器">
|
||||||
|
<div class="timer-display">
|
||||||
|
<span class="time-main">{{ formatTimeMain(data.timeDisplay) }}</span>
|
||||||
|
<span class="time-milliseconds">{{ formatTimeMilliseconds(data.timeDisplay) }}</span>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 控制按钮 -->
|
||||||
|
<n-card size="small" title="控制">
|
||||||
|
<n-flex justify="center" gap="12">
|
||||||
|
<n-button
|
||||||
|
type="success"
|
||||||
|
:disabled="data.isRunning"
|
||||||
|
@click="startTimer"
|
||||||
|
>开始</n-button>
|
||||||
|
<n-button
|
||||||
|
type="warning"
|
||||||
|
:disabled="!data.isRunning"
|
||||||
|
@click="pauseTimer"
|
||||||
|
>暂停</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
@click="resetTimer"
|
||||||
|
>重置</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="recordTime"
|
||||||
|
>记录</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 设置 -->
|
||||||
|
<n-card size="small" title="设置">
|
||||||
|
<n-form
|
||||||
|
class="form-no-feedback"
|
||||||
|
label-align="left"
|
||||||
|
label-placement="top"
|
||||||
|
label-width="auto"
|
||||||
|
>
|
||||||
|
<!-- 计时模式 -->
|
||||||
|
<n-form-item label="计时模式">
|
||||||
|
<n-radio-group v-model:value="data.timerMode">
|
||||||
|
<n-radio-button label="正计时" :value="'countUp'" />
|
||||||
|
<n-radio-button label="倒计时" :value="'countDown'" />
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 倒计时时间设置 -->
|
||||||
|
<n-form-item label="倒计时设置" v-if="data.timerMode === 'countDown'">
|
||||||
|
<n-input-group>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="data.countDownTime.hours"
|
||||||
|
:min="0"
|
||||||
|
:max="99"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
<span class="time-separator">时</span>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="data.countDownTime.minutes"
|
||||||
|
:min="0"
|
||||||
|
:max="59"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
<span class="time-separator">分</span>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="data.countDownTime.seconds"
|
||||||
|
:min="0"
|
||||||
|
:max="59"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
></n-input-number>
|
||||||
|
<span class="time-separator">秒</span>
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 时间记录 -->
|
||||||
|
<n-card size="small" title="时间记录">
|
||||||
|
<div class="records-content">
|
||||||
|
<div
|
||||||
|
v-if="data.records.length === 0"
|
||||||
|
class="no-records"
|
||||||
|
>暂无记录</div>
|
||||||
|
<div
|
||||||
|
v-for="(record, index) in data.records"
|
||||||
|
:key="record.id"
|
||||||
|
class="record-item"
|
||||||
|
>
|
||||||
|
<span class="record-index">{{ index + 1 }}</span>
|
||||||
|
<span class="record-time">{{ formatTime(record.time) }}</span>
|
||||||
|
<span class="record-timestamp">{{ formatDateTime(record.timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
size="small"
|
||||||
|
class="clear-records-btn"
|
||||||
|
@click="clearRecords"
|
||||||
|
>清空记录</n-button>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NButton, NCard, NFlex, NForm, NFormItem, NInputGroup, NInputNumber, NRadioButton, NRadioGroup,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reactive, onBeforeUnmount, watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCommonDateTime,
|
||||||
|
} from '@frost-utils/javascript/common/index';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$dialog, $notification,
|
||||||
|
} from '@/assets/js/naive-ui';
|
||||||
|
|
||||||
|
/** 数据 */
|
||||||
|
const data = reactive({
|
||||||
|
/** 计时器模式:countUp(正计时), countDown(倒计时) */
|
||||||
|
timerMode: 'countUp',
|
||||||
|
/** 是否正在运行 */
|
||||||
|
isRunning: false,
|
||||||
|
/** 当前显示时间(毫秒) */
|
||||||
|
timeDisplay: 0,
|
||||||
|
/** 倒计时时间设置 */
|
||||||
|
countDownTime: {
|
||||||
|
hours: 0,
|
||||||
|
minutes: 5,
|
||||||
|
seconds: 0
|
||||||
|
},
|
||||||
|
/** 计时器ID */
|
||||||
|
timerID: null,
|
||||||
|
/** 开始时间戳 */
|
||||||
|
startTime: 0,
|
||||||
|
/** 已运行时间 */
|
||||||
|
elapsedTime: 0,
|
||||||
|
/** 时间记录 */
|
||||||
|
records: [],
|
||||||
|
/** 记录ID */
|
||||||
|
recordID: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 格式化时间显示(主时间部分:HH:MM:SS) */
|
||||||
|
function formatTimeMain(milliseconds) {
|
||||||
|
|
||||||
|
let totalSeconds = Math.floor(milliseconds / 1000);
|
||||||
|
let hours = Math.floor(totalSeconds / 3600);
|
||||||
|
let minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
let seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化时间显示(毫秒部分:.SSS) */
|
||||||
|
function formatTimeMilliseconds(milliseconds) {
|
||||||
|
let ms = milliseconds % 1000;
|
||||||
|
return `.${ms.toString().padStart(3, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化完整时间显示(用于记录和提示) */
|
||||||
|
function formatTime(milliseconds) {
|
||||||
|
return `${formatTimeMain(milliseconds)}${formatTimeMilliseconds(milliseconds)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化日期时间 */
|
||||||
|
function formatDateTime(timestamp) {
|
||||||
|
return getCommonDateTime(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开始计时器 */
|
||||||
|
function startTimer() {
|
||||||
|
|
||||||
|
if (data.timerMode === 'countUp') {
|
||||||
|
|
||||||
|
// 正计时模式
|
||||||
|
data.startTime = Date.now() - data.elapsedTime;
|
||||||
|
data.timerID = setInterval(() => {
|
||||||
|
data.elapsedTime = Date.now() - data.startTime;
|
||||||
|
data.timeDisplay = data.elapsedTime;
|
||||||
|
}, 10); // 每10毫秒更新一次,提高毫秒显示精度
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// 倒计时模式
|
||||||
|
if (data.timeDisplay === 0) {
|
||||||
|
// 如果是首次开始或重置后开始,设置倒计时时间
|
||||||
|
const { hours, minutes, seconds } = data.countDownTime;
|
||||||
|
data.timeDisplay = (hours * 3600 + minutes * 60 + seconds) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.startTime = Date.now() + data.timeDisplay;
|
||||||
|
data.timerID = setInterval(() => {
|
||||||
|
|
||||||
|
data.timeDisplay = Math.max(0, data.startTime - Date.now());
|
||||||
|
data.elapsedTime = (data.startTime - data.timeDisplay) - data.startTime + data.timeDisplay;
|
||||||
|
|
||||||
|
// 倒计时结束
|
||||||
|
if (data.timeDisplay === 0) {
|
||||||
|
pauseTimer();
|
||||||
|
notify({
|
||||||
|
message: '倒计时结束!',
|
||||||
|
type: 'success',
|
||||||
|
title: '提示'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 10); // 每10毫秒更新一次,提高毫秒显示精度
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data.isRunning = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 暂停计时器 */
|
||||||
|
function pauseTimer() {
|
||||||
|
if (data.timerID) {
|
||||||
|
clearInterval(data.timerID);
|
||||||
|
data.timerID = null;
|
||||||
|
data.isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置计时器 */
|
||||||
|
function resetTimer() {
|
||||||
|
pauseTimer();
|
||||||
|
data.elapsedTime = 0;
|
||||||
|
data.timeDisplay = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录时间 */
|
||||||
|
function recordTime() {
|
||||||
|
|
||||||
|
data.recordID += 1;
|
||||||
|
data.records.push({
|
||||||
|
id: data.recordID,
|
||||||
|
time: data.timeDisplay,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
notify({
|
||||||
|
message: `已记录时间: ${formatTime(data.timeDisplay)}`,
|
||||||
|
type: 'info',
|
||||||
|
title: '记录成功'
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空记录 */
|
||||||
|
function clearRecords() {
|
||||||
|
$dialog.create({
|
||||||
|
content: '确定要清空所有时间记录吗?',
|
||||||
|
negativeText: '取消',
|
||||||
|
positiveText: '确定',
|
||||||
|
title: '确认',
|
||||||
|
type: 'default',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
data.records = [];
|
||||||
|
data.recordID = 0;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 提示信息
|
||||||
|
* @param {object} options
|
||||||
|
* @param {number} options.duration
|
||||||
|
* @param {string} options.message
|
||||||
|
* @param {string} options.title
|
||||||
|
* @param {string} options.type
|
||||||
|
*/
|
||||||
|
function notify(options) {
|
||||||
|
|
||||||
|
let {
|
||||||
|
duration = 3000,
|
||||||
|
message = '',
|
||||||
|
title = '提示',
|
||||||
|
type = 'info',
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return $notification.create({
|
||||||
|
content: message,
|
||||||
|
duration: duration,
|
||||||
|
title: title,
|
||||||
|
type: type,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听计时模式切换 */
|
||||||
|
watch(
|
||||||
|
() => data.timerMode,
|
||||||
|
() => {
|
||||||
|
// 切换模式时重置计时器
|
||||||
|
resetTimer();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 监听倒计时时间设置变化 */
|
||||||
|
watch(
|
||||||
|
() => [data.countDownTime.hours, data.countDownTime.minutes, data.countDownTime.seconds],
|
||||||
|
() => {
|
||||||
|
// 只有在倒计时模式且计时器未运行时,才更新显示时间
|
||||||
|
if (data.timerMode === 'countDown' && !data.isRunning && data.timeDisplay === 0) {
|
||||||
|
const { hours, minutes, seconds } = data.countDownTime;
|
||||||
|
data.timeDisplay = (hours * 3600 + minutes * 60 + seconds) * 1000;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (data.timerID) {
|
||||||
|
clearInterval(data.timerID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.timer-display {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
color: #2196F3;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.time-main {
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-milliseconds {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-separator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-content {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #F0F0F0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #FFF;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.record-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px dashed #F0F0F0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-index {
|
||||||
|
width: 30px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-time {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2196F3;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-timestamp {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-records {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-records-btn {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user