Skip to content

Commit

Permalink
[timer] 增加计时器
Browse files Browse the repository at this point in the history
  • Loading branch information
masquevil committed Jul 7, 2024
1 parent bfe6462 commit 6ac9bdc
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 0 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [包含工具](#包含工具)
- [车卡工具](#车卡工具)
- [KP 招募 PL 展示器](#kp-招募-pl-展示器)
- [计时加速器(可朗读)](#计时加速器可朗读)
- [记录工具](#记录工具)
- [Project Setup](#project-setup)
- [商业使用](#商业使用)
Expand Down Expand Up @@ -34,6 +35,16 @@
4. (孩子不想搞服务器啊!后端不算特别熟,加上之前被人攻击了一波,感觉维护成本太高了,纯前端项目就没那么容易搞多人的)
5. (接下来应该会用 github gist 支持一定程度上的数据库,但是需要各位会用 github 才能加入自己的数据)

### 计时加速器(可朗读)

![example](./src/assets/images/readme-preview/timer.png)

1. 用于 TRPG 游戏中的计时
2. 自动朗读
3. 可以加速特定时间
4. 可以暂停、继续
5. 结束计时可以朗读指定内容

### 记录工具

![example](./src/assets/images/readme-preview/record.png)
Expand Down
9 changes: 9 additions & 0 deletions src/apps/home/AppView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { RouteLocationRaw } from 'vue-router';
import cocCardPreview from '@/assets/images/tools-preview/coc-card.jpg';
import recordPreview from '@/assets/images/tools-preview/record.png';
import kpAdsPreview from '@/assets/images/tools-preview/kp-ads.png';
import timerPreview from '@/assets/images/tools-preview/timer.png';
interface AppConfig {
key: string;
Expand All @@ -23,6 +24,14 @@ const appConfigs: Record<'online' | 'offline', AppConfig[]> = {
},
preview: cocCardPreview,
},
{
key: 'timer',
name: '计时器',
to: {
name: 'timer',
},
preview: timerPreview,
},
],
offline: [
{
Expand Down
233 changes: 233 additions & 0 deletions src/apps/timer/AppView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useIntervalFn } from '@vueuse/core';
import speak from './speak';
const settingMinute = ref();
const settingSecond = ref(0);
const workingTime = ref(0);
const speedUpSecond = ref();
const stopTip = ref();
const speakSpeeds = {
slow: 1,
normal: 1.2,
fast: 1.4,
};
function getTimeInfo(time: number) {
const minute = Math.floor(time / 60);
const second = time % 60;
return { minute, second };
}
function getSpeekTime(minute: number, second: number) {
return `${minute ? `${minute}分` : ''}${second ? `${second}秒` : ''}`;
}
function getDisplayTime(minute: number, second: number) {
return `${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
}
const workingTimeInfo = computed(() => {
return {
minute: Math.floor(workingTime.value / 60),
second: workingTime.value % 60,
};
});
const speekWorkingTime = computed(() => {
return getSpeekTime(workingTimeInfo.value.minute, workingTimeInfo.value.second);
});
const displayWorkingTime = computed(() => {
return getDisplayTime(workingTimeInfo.value.minute, workingTimeInfo.value.second);
});
const { isActive, pause, resume } = useIntervalFn(
() => {
workingTime.value -= 1;
if (workingTime.value <= 0) {
workingTime.value = 0;
speak(stopTip.value || '计时结束', { rate: speakSpeeds.normal });
pause();
} else if (workingTime.value % 60 === 0) {
speak(`剩余${speekWorkingTime.value}`, { rate: speakSpeeds.fast });
} else if (workingTime.value === 30) {
speak(`${speekWorkingTime.value}`, { rate: speakSpeeds.fast });
} else if (workingTime.value === 10) {
speak(`${speekWorkingTime.value}`, { rate: speakSpeeds.fast });
} else if (workingTime.value <= 3) {
speak(`${workingTime.value}`, { rate: speakSpeeds.fast });
}
},
1000,
{ immediate: false },
);
function onSetMinute() {
if (isNaN(settingMinute.value) && isNaN(settingSecond.value)) {
return speak('请输入数字', { rate: speakSpeeds.normal });
}
const time = (settingMinute.value || 0) * 60 + (settingSecond.value || 0);
if (time <= 0) {
return speak('请大于0', { rate: speakSpeeds.normal });
}
const { minute, second } = getTimeInfo(time);
const speekTime = getSpeekTime(minute, second);
speak(`开始计时${speekTime}`, { rate: speakSpeeds.fast });
workingTime.value = time;
resume();
}
function onSpeedUp() {
const time = Number(speedUpSecond.value);
if (isNaN(time)) {
return speak('请输入数字', { rate: speakSpeeds.normal });
}
workingTime.value -= time;
speak(`剩余${speekWorkingTime.value}`, { rate: speakSpeeds.fast });
}
function onPauseOrResume() {
if (isActive.value) {
pause();
speak('暂停计时', { rate: speakSpeeds.normal });
} else {
resume();
speak(`恢复计时${speekWorkingTime.value}`, { rate: speakSpeeds.fast });
}
}
function onStop() {
workingTime.value = 0;
speak('停止计时', { rate: speakSpeeds.normal });
pause();
}
</script>

<template>
<main class="page">
<div class="timer">
<div>{{ displayWorkingTime }}</div>
</div>
<div class="action-row">
<form
class="action-card"
@submit.prevent="onSetMinute"
>
<div class="action-card-title">设置倒计时</div>
<div class="action-card-action">
<el-input
type="number"
size="large"
v-model="settingMinute"
placeholder="输入分钟数"
/>
<el-input
type="number"
size="large"
v-model="settingSecond"
placeholder="输入秒数"
/>
<el-button
type="default"
size="large"
nativeType="submit"
>
开始计时
</el-button>
</div>
</form>
<form
class="action-card"
@submit.prevent="onSpeedUp"
>
<div class="action-card-title">为倒计时加速</div>
<div class="action-card-action">
<el-input
type="number"
size="large"
v-model="speedUpSecond"
placeholder="输入秒数"
/>
<el-button
type="default"
size="large"
nativeType="submit"
>
立刻加速
</el-button>
</div>
</form>
<div class="action-card">
<div class="action-card-title">倒计时控制器</div>
<div class="action-card-action">
<el-input
type="string"
size="large"
v-model="stopTip"
placeholder="计时结束后的提示"
/>
<el-button
type="default"
size="large"
:disabled="workingTime <= 0"
@click="onPauseOrResume"
>
<template v-if="workingTime <= 0">计时已停止</template>
<template v-else>{{ isActive ? '暂停' : '恢复' }}</template>
</el-button>
<el-button
type="default"
size="large"
v-if="workingTime > 0"
:style="{ marginLeft: '0' }"
@click="onStop"
>
停止
</el-button>
</div>
</div>
</div>
</main>
</template>

<style scoped lang="scss">
.page {
max-width: 1120px;
margin: auto;
padding: 12px;
font-size: 14px;
}
.timer {
--timer-height: calc(100vh - 100px - 48px);
margin-bottom: 12px;
width: 100%;
height: var(--timer-height);
font-size: min(var(--timer-height), 30vw);
line-height: 1;
display: flex;
justify-content: center;
align-items: center;
}
.action-row {
display: flex;
align-items: center;
gap: 12px;
}
.action-card {
max-width: 400px;
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
border: 1px solid var(--color-border);
border-radius: 4px;
background-color: var(--color-bg);
}
.action-card-title {
font-size: 16px;
}
.action-card-action {
display: flex;
align-items: center;
gap: 8px;
}
</style>
34 changes: 34 additions & 0 deletions src/apps/timer/speak.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
interface VoiceConfig {
lang?: string;
// 音量
vol?: number;
// 语速
rate?: number;
// 音高
pitch?: number;
}

window.speechSynthesis.getVoices();

// 函数
export default function speak(text: string, config: VoiceConfig = {}) {
const { lang = 'zh-CN', vol = 1, rate = 1, pitch = 1 } = config;
// 播报前取消之前的播报
window.speechSynthesis.cancel();
// 实例化播报内容
const instance = new SpeechSynthesisUtterance(text);
instance.text = text;
instance.lang = lang;
instance.volume = vol;
instance.rate = rate;
instance.pitch = pitch;
// 选择语音
const listArr = window.speechSynthesis.getVoices();
const preferredVoice = listArr.find((item) => item.name === 'Google 普通话(中国大陆)');
console.log('xxx', preferredVoice, listArr);
if (preferredVoice) {
instance.voice = preferredVoice;
}
window.speechSynthesis.speak(instance);
// instance.addEventListener("end", () => {});
}
Binary file added src/assets/images/readme-preview/timer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/tools-preview/timer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ const router = createRouter({
name: 'kp-ads',
component: () => import('../apps/kp-ads/AppView.vue'),
},
{
path: '/timer',
name: 'timer',
component: () => import('../apps/timer/AppView.vue'),
},
{
path: '/tfg-stories',
name: 'tfg-stories',
Expand Down

0 comments on commit 6ac9bdc

Please sign in to comment.