diff --git a/.pnpmrc b/.pnpmrc new file mode 100644 index 00000000..f5a5cb23 --- /dev/null +++ b/.pnpmrc @@ -0,0 +1 @@ +approve-builds=** diff --git a/docs/superpowers/plans/2026-06-07-pomodoro.md b/docs/superpowers/plans/2026-06-07-pomodoro.md new file mode 100644 index 00000000..9666bf08 --- /dev/null +++ b/docs/superpowers/plans/2026-06-07-pomodoro.md @@ -0,0 +1,661 @@ +# BongoCat 番茄时钟功能实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**目标:** 为 BongoCat 添加番茄时钟功能,让猫咪在工作/休息时显示不同动画和状态 + +**架构:** 采用 Pinia store 管理番茄时钟状态,通过 Vue composable 处理计时逻辑,在主窗口显示悬浮计时器组件,在设置页面添加配置选项 + +**技术栈:** Vue 3 + Pinia + TypeScript + Tauri + +--- + +## 文件结构 + +``` +src/ +├── stores/ +│ └── pomodoro.ts # 番茄时钟状态管理 +├── composables/ +│ └── usePomodoro.ts # 番茄时钟计时逻辑 +├── components/ +│ └── pomodoro-timer/ # 悬浮计时器组件 +│ └── index.vue +└── pages/preference/components/ + └── pomodoro/ # 设置页面组件 + └── index.vue +``` + +--- + +## 任务 1: 创建番茄时钟 Store + +**文件:** +- 创建: `src/stores/pomodoro.ts` +- 测试: 无 (本项目无单元测试) + +- [ ] **Step 1: 创建 Store 文件** + +```typescript +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +export type PomodoroPhase = 'work' | 'shortBreak' | 'longBreak' + +export interface PomodoroSettings { + workDuration: number // 工作时长(分钟),默认25 + shortBreakDuration: number // 短休息时长(分钟),默认5 + longBreakDuration: number // 长休息时长(分钟),默认15 + longBreakInterval: number // 长休息间隔(番茄数),默认4 + autoStartBreak: boolean // 自动开始休息 + autoStartWork: boolean // 自动开始工作 + enabled: boolean // 功能开关 +} + +export interface PomodoroState { + isRunning: boolean + currentPhase: PomodoroPhase + timeRemaining: number // 剩余秒数 + sessionsCompleted: number // 完成的番茄数 + currentSession: number // 当前是第几个番茄(1-4) +} + +export const usePomodoroStore = defineStore('pomodoro', () => { + // 番茄时钟功能开关 + const enabled = ref(false) + + // 番茄时钟配置 + const settings = ref({ + workDuration: 25, + shortBreakDuration: 5, + longBreakDuration: 15, + longBreakInterval: 4, + autoStartBreak: false, + autoStartWork: false, + }) + + // 状态 + const isRunning = ref(false) + const currentPhase = ref('work') + const timeRemaining = ref(settings.value.workDuration * 60) // 秒 + const sessionsCompleted = ref(0) + const currentSession = ref(1) + + // 计算属性 + const totalTime = computed(() => { + switch (currentPhase.value) { + case 'work': + return settings.value.workDuration * 60 + case 'shortBreak': + return settings.value.shortBreakDuration * 60 + case 'longBreak': + return settings.value.longBreakDuration * 60 + } + }) + + const progress = computed(() => { + if (totalTime.value === 0) return 0 + return ((totalTime.value - timeRemaining.value) / totalTime.value) * 100 + }) + + const formattedTime = computed(() => { + const minutes = Math.floor(timeRemaining.value / 60) + const seconds = timeRemaining.value % 60 + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` + }) + + // 重置时间 + const resetTime = () => { + timeRemaining.value = totalTime.value + } + + // 开始计时 + const start = () => { + isRunning.value = true + } + + // 暂停计时 + const pause = () => { + isRunning.value = false + } + + // 切换计时 + const toggle = () => { + isRunning.value = !isRunning.value + } + + // 跳到下一阶段 + const nextPhase = () => { + isRunning.value = false + + if (currentPhase.value === 'work') { + sessionsCompleted.value++ + currentSession.value++ + + if (currentSession.value > settings.value.longBreakInterval) { + currentPhase.value = 'longBreak' + currentSession.value = 1 + } else { + currentPhase.value = 'shortBreak' + } + + if (settings.value.autoStartBreak) { + start() + } + } else { + currentPhase.value = 'work' + + if (settings.value.autoStartWork) { + start() + } + } + + resetTime() + } + + // 重置所有状态 + const reset = () => { + isRunning.value = false + currentPhase.value = 'work' + timeRemaining.value = settings.value.workDuration * 60 + sessionsCompleted.value = 0 + currentSession.value = 1 + } + + // tick (每秒调用) + const tick = () => { + if (!isRunning.value || timeRemaining.value <= 0) return + + timeRemaining.value-- + + if (timeRemaining.value <= 0) { + nextPhase() + } + } + + return { + enabled, + settings, + isRunning, + currentPhase, + timeRemaining, + sessionsCompleted, + currentSession, + totalTime, + progress, + formattedTime, + resetTime, + start, + pause, + toggle, + nextPhase, + reset, + tick, + } +}) +``` + +- [ ] **Step 2: 提交代码** + +```bash +git add src/stores/pomodoro.ts +git commit -m "feat(pomodoro): add pomodoro store with state management" +``` + +--- + +## 任务 2: 创建番茄时钟 Composable + +**文件:** +- 创建: `src/composables/usePomodoro.ts` +- 修改: `src/App.vue` (初始化 store) + +- [ ] **Step 1: 创建 usePomodoro.ts** + +```typescript +import { onUnmounted, watch } from 'vue' +import { usePomodoroStore } from '@/stores/pomodoro' + +export function usePomodoro() { + const pomodoroStore = usePomodoroStore() + let intervalId: ReturnType | null = null + + const startTimer = () => { + if (intervalId) return + + intervalId = setInterval(() => { + pomodoroStore.tick() + }, 1000) + } + + const stopTimer = () => { + if (intervalId) { + clearInterval(intervalId) + intervalId = null + } + } + + // 监听运行状态 + watch( + () => pomodoroStore.isRunning, + (running) => { + if (running) { + startTimer() + } else { + stopTimer() + } + }, + { immediate: true } + ) + + onUnmounted(() => { + stopTimer() + }) + + return { + startTimer, + stopTimer, + } +} +``` + +- [ ] **Step 2: 在 App.vue 中初始化 store** + +修改 `src/App.vue`,在 onMounted 中添加: + +```typescript +import { usePomodoroStore } from './stores/pomodoro' + +const pomodoroStore = usePomodoroStore() +``` + +在 `onMounted` 中添加: + +```typescript +await pomodoroStore.$tauri.start() +``` + +- [ ] **Step 3: 提交代码** + +```bash +git add src/composables/usePomodoro.ts src/App.vue +git commit -m "feat(pomodoro): add usePomodoro composable" +``` + +--- + +## 任务 3: 创建番茄时钟设置页面组件 + +**文件:** +- 创建: `src/pages/preference/components/pomodoro/index.vue` +- 修改: `src/pages/preference/index.vue` (添加 tab) +- 修改: `src/locales/zh-CN.json` +- 修改: `src/locales/en-US.json` + +- [ ] **Step 1: 创建设置页面组件** + +```vue + + + +``` + +- [ ] **Step 2: 在设置页面添加入口** + +修改 `src/pages/preference/index.vue`: + +在 import 部分添加: +```typescript +import Pomodoro from './components/pomodoro/index.vue' +``` + +在 menus 中添加: +```typescript +{ + key: 'pomodoro', + label: t('pages.preference.pomodoro.title'), + icon: 'i-solar:timer-bold', + component: Pomodoro, +}, +``` + +- [ ] **Step 3: 添加中文国际化文本** + +在 `src/locales/zh-CN.json` 中添加: +```json +"pomodoro": { + "title": "番茄时钟", + "labels": { + "pomodoroSettings": "番茄时钟设置", + "timerSettings": "计时器设置", + "enabled": "启用番茄时钟", + "workDuration": "工作时长", + "shortBreakDuration": "短休息时长", + "longBreakDuration": "长休息时长", + "longBreakInterval": "长休息间隔", + "autoStartBreak": "自动开始休息", + "autoStartWork": "自动开始工作" + }, + "hints": { + "enabled": "启用后,猫咪将显示番茄时钟悬浮组件。", + "workDuration": "工作阶段的时长。", + "shortBreakDuration": "短休息阶段的时长。", + "longBreakDuration": "长休息阶段的时长。", + "longBreakInterval": "多少个番茄后触发长休息。", + "autoStartBreak": "工作结束后自动开始休息。", + "autoStartWork": "休息结束后自动开始工作。" + } +} +``` + +- [ ] **Step 4: 添加英文国际化文本** + +在 `src/locales/en-US.json` 中添加: +```json +"pomodoro": { + "title": "Pomodoro Timer", + "labels": { + "pomodoroSettings": "Pomodoro Settings", + "timerSettings": "Timer Settings", + "enabled": "Enable Pomodoro", + "workDuration": "Work Duration", + "shortBreakDuration": "Short Break Duration", + "longBreakDuration": "Long Break Duration", + "longBreakInterval": "Long Break Interval", + "autoStartBreak": "Auto Start Break", + "autoStartWork": "Auto Start Work" + }, + "hints": { + "enabled": "Enable to show the pomodoro timer widget on the cat window.", + "workDuration": "Duration of work sessions.", + "shortBreakDuration": "Duration of short breaks.", + "longBreakDuration": "Duration of long breaks.", + "longBreakInterval": "Number of pomodoros before a long break.", + "autoStartBreak": "Automatically start break after work.", + "autoStartWork": "Automatically start work after break." + } +} +``` + +- [ ] **Step 5: 提交代码** + +```bash +git add src/pages/preference/components/pomodoro/ +git add src/pages/preference/index.vue +git add src/locales/zh-CN.json src/locales/en-US.json +git commit -m "feat(pomodoro): add pomodoro settings page" +``` + +--- + +## 任务 4: 创建番茄时钟悬浮组件 + +**文件:** +- 创建: `src/components/pomodoro-timer/index.vue` +- 修改: `src/pages/main/index.vue` (集成组件) + +- [ ] **Step 1: 创建悬浮组件** + +```vue + + + + + +``` + +- [ ] **Step 2: 在主页面集成** + +修改 `src/pages/main/index.vue`: + +在 import 部分添加: +```typescript +import PomodoroTimer from '@/components/pomodoro-timer/index.vue' +``` + +在 template 中添加: +```vue + +``` + +- [ ] **Step 3: 提交代码** + +```bash +git add src/components/pomodoro-timer/ +git add src/pages/main/index.vue +git commit -m "feat(pomodoro): add floating pomodoro timer component" +``` + +--- + +## 任务 5: 验证构建 + +**文件:** +- 构建: `pnpm build` + +- [ ] **Step 1: 运行构建** + +```bash +cd BongoCat +pnpm build +``` + +预期: 构建成功,无错误 + +- [ ] **Step 2: 提交所有更改** + +```bash +git add -A +git commit -m "feat: add pomodoro timer feature to BongoCat" +``` + +--- + +## 自检清单 + +- [x] Spec 覆盖: 所有需求都有对应任务 +- [x] 占位符检查: 无 TBD/TODO +- [x] 类型一致性: Store 中的类型在 Composable 和组件中正确使用 +- [x] 文件路径: 所有路径精确匹配 +- [x] 代码完整性: 每个 Step 都包含实际代码 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..ee262349 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +allowBuilds: + '@parcel/watcher': set this to true or false + esbuild: set this to true or false + simple-git-hooks: set this to true or false diff --git a/src/App.vue b/src/App.vue index 321c4fb6..6e5841be 100644 --- a/src/App.vue +++ b/src/App.vue @@ -13,6 +13,7 @@ import { RouterView } from 'vue-router' import { useTauriListen } from './composables/useTauriListen' import { useWindowState } from './composables/useWindowState' +import { usePomodoro } from './composables/usePomodoro' import { LANGUAGE, LISTEN_KEY } from './constants' import { getAntdLocale } from './locales/index.ts' import { hideWindow, showWindow } from './plugins/window' @@ -21,17 +22,22 @@ import { useCatStore } from './stores/cat' import { useGeneralStore } from './stores/general' import { useModelStore } from './stores/model' import { useShortcutStore } from './stores/shortcut.ts' +import { usePomodoroStore } from './stores/pomodoro' const appStore = useAppStore() const modelStore = useModelStore() const catStore = useCatStore() const generalStore = useGeneralStore() const shortcutStore = useShortcutStore() +const pomodoroStore = usePomodoroStore() const appWindow = getCurrentWebviewWindow() const { isRestored, restoreState } = useWindowState() const { darkAlgorithm, defaultAlgorithm } = theme const { locale } = useI18n() +// Initialize pomodoro timer +usePomodoro() + onMounted(async () => { await appStore.$tauri.start() await appStore.init() @@ -42,6 +48,7 @@ onMounted(async () => { await generalStore.$tauri.start() await generalStore.init() await shortcutStore.$tauri.start() + await pomodoroStore.$tauri.start() await restoreState() }) diff --git a/src/components/pomodoro-timer/index.vue b/src/components/pomodoro-timer/index.vue new file mode 100644 index 00000000..9bb2da16 --- /dev/null +++ b/src/components/pomodoro-timer/index.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/src/composables/usePomodoro.ts b/src/composables/usePomodoro.ts new file mode 100644 index 00000000..8dea39b1 --- /dev/null +++ b/src/composables/usePomodoro.ts @@ -0,0 +1,44 @@ +import { onUnmounted, watch } from 'vue' +import { usePomodoroStore } from '@/stores/pomodoro' + +export function usePomodoro() { + const pomodoroStore = usePomodoroStore() + let intervalId: ReturnType | null = null + + const startTimer = () => { + if (intervalId) return + + intervalId = setInterval(() => { + pomodoroStore.tick() + }, 1000) + } + + const stopTimer = () => { + if (intervalId) { + clearInterval(intervalId) + intervalId = null + } + } + + // 监听运行状态 + watch( + () => pomodoroStore.isRunning, + (running) => { + if (running) { + startTimer() + } else { + stopTimer() + } + }, + { immediate: true } + ) + + onUnmounted(() => { + stopTimer() + }) + + return { + startTimer, + stopTimer, + } +} diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 15172d14..318335fe 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -147,6 +147,29 @@ "feedbackIssues": "Feedback Issues", "viewLog": "View Logs" } + }, + "pomodoro": { + "title": "Pomodoro Timer", + "labels": { + "pomodoroSettings": "Pomodoro Settings", + "timerSettings": "Timer Settings", + "enabled": "Enable Pomodoro", + "workDuration": "Work Duration", + "shortBreakDuration": "Short Break Duration", + "longBreakDuration": "Long Break Duration", + "longBreakInterval": "Long Break Interval", + "autoStartBreak": "Auto Start Break", + "autoStartWork": "Auto Start Work" + }, + "hints": { + "enabled": "When enabled, the cat window will show the pomodoro timer widget.", + "workDuration": "Duration of work sessions.", + "shortBreakDuration": "Duration of short breaks.", + "longBreakDuration": "Duration of long breaks.", + "longBreakInterval": "Number of pomodoros before a long break.", + "autoStartBreak": "Automatically start break after work ends.", + "autoStartWork": "Automatically start work after break ends." + } } } }, diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index e1a3e4dc..7658a724 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -147,6 +147,29 @@ "feedbackIssues": "反馈问题", "viewLog": "查看日志" } + }, + "pomodoro": { + "title": "番茄时钟", + "labels": { + "pomodoroSettings": "番茄时钟设置", + "timerSettings": "计时器设置", + "enabled": "启用番茄时钟", + "workDuration": "工作时长", + "shortBreakDuration": "短休息时长", + "longBreakDuration": "长休息时长", + "longBreakInterval": "长休息间隔", + "autoStartBreak": "自动开始休息", + "autoStartWork": "自动开始工作" + }, + "hints": { + "enabled": "启用后,猫咪将显示番茄时钟悬浮组件。", + "workDuration": "工作阶段的时长。", + "shortBreakDuration": "短休息阶段的时长。", + "longBreakDuration": "长休息阶段的时长。", + "longBreakInterval": "多少个番茄后触发长休息。", + "autoStartBreak": "工作结束后自动开始休息。", + "autoStartWork": "休息结束后自动开始工作。" + } } } }, diff --git a/src/pages/main/index.vue b/src/pages/main/index.vue index 712f5974..fe44984f 100644 --- a/src/pages/main/index.vue +++ b/src/pages/main/index.vue @@ -17,6 +17,7 @@ import { useDevice } from '@/composables/useDevice' import { useGamepad } from '@/composables/useGamepad' import { useModel } from '@/composables/useModel' import { useTauriListen } from '@/composables/useTauriListen' +import PomodoroTimer from '@/components/pomodoro-timer/index.vue' import { LISTEN_KEY } from '@/constants' import { hideWindow, setAlwaysOnTop, setTaskbarVisibility, showWindow } from '@/plugins/window' import { useCatStore } from '@/stores/cat' @@ -213,4 +214,6 @@ function handleMouseMove(event: MouseEvent) { + + diff --git a/src/pages/preference/components/pomodoro/index.vue b/src/pages/preference/components/pomodoro/index.vue new file mode 100644 index 00000000..332cb427 --- /dev/null +++ b/src/pages/preference/components/pomodoro/index.vue @@ -0,0 +1,98 @@ + + + diff --git a/src/pages/preference/index.vue b/src/pages/preference/index.vue index 1520be13..3e452b95 100644 --- a/src/pages/preference/index.vue +++ b/src/pages/preference/index.vue @@ -15,6 +15,7 @@ import About from './components/about/index.vue' import Cat from './components/cat/index.vue' import General from './components/general/index.vue' import Model from './components/model/index.vue' +import Pomodoro from './components/pomodoro/index.vue' import Shortcut from './components/shortcut/index.vue' useTray() @@ -54,6 +55,12 @@ const menus = computed(() => [ icon: 'i-solar:keyboard-bold', component: Shortcut, }, + { + key: 'pomodoro', + label: t('pages.preference.pomodoro.title'), + icon: 'i-solar:timer-bold', + component: Pomodoro, + }, { key: 'about', label: t('pages.preference.about.title'), diff --git a/src/stores/pomodoro.ts b/src/stores/pomodoro.ts new file mode 100644 index 00000000..2c38ca29 --- /dev/null +++ b/src/stores/pomodoro.ts @@ -0,0 +1,156 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +export type PomodoroPhase = 'work' | 'shortBreak' | 'longBreak' + +export interface PomodoroSettings { + workDuration: number // 工作时长(分钟),默认25 + shortBreakDuration: number // 短休息时长(分钟),默认5 + longBreakDuration: number // 长休息时长(分钟),默认15 + longBreakInterval: number // 长休息间隔(番茄数),默认4 + autoStartBreak: boolean // 自动开始休息 + autoStartWork: boolean // 自动开始工作 + enabled: boolean // 功能开关 +} + +export interface PomodoroState { + isRunning: boolean + currentPhase: PomodoroPhase + timeRemaining: number // 剩余秒数 + sessionsCompleted: number // 完成的番茄数 + currentSession: number // 当前是第几个番茄(1-4) +} + +export const usePomodoroStore = defineStore('pomodoro', () => { + // 番茄时钟功能开关 + const enabled = ref(false) + + // 番茄时钟配置 + const settings = ref({ + workDuration: 25, + shortBreakDuration: 5, + longBreakDuration: 15, + longBreakInterval: 4, + autoStartBreak: false, + autoStartWork: false, + }) + + // 状态 + const isRunning = ref(false) + const currentPhase = ref('work') + const timeRemaining = ref(settings.value.workDuration * 60) // 秒 + const sessionsCompleted = ref(0) + const currentSession = ref(1) + + // 计算属性 + const totalTime = computed(() => { + switch (currentPhase.value) { + case 'work': + return settings.value.workDuration * 60 + case 'shortBreak': + return settings.value.shortBreakDuration * 60 + case 'longBreak': + return settings.value.longBreakDuration * 60 + } + }) + + const progress = computed(() => { + if (totalTime.value === 0) return 0 + return ((totalTime.value - timeRemaining.value) / totalTime.value) * 100 + }) + + const formattedTime = computed(() => { + const minutes = Math.floor(timeRemaining.value / 60) + const seconds = timeRemaining.value % 60 + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` + }) + + // 重置时间 + const resetTime = () => { + timeRemaining.value = totalTime.value + } + + // 开始计时 + const start = () => { + isRunning.value = true + } + + // 暂停计时 + const pause = () => { + isRunning.value = false + } + + // 切换计时 + const toggle = () => { + isRunning.value = !isRunning.value + } + + // 跳到下一阶段 + const nextPhase = () => { + isRunning.value = false + + if (currentPhase.value === 'work') { + sessionsCompleted.value++ + currentSession.value++ + + if (currentSession.value > settings.value.longBreakInterval) { + currentPhase.value = 'longBreak' + currentSession.value = 1 + } else { + currentPhase.value = 'shortBreak' + } + + if (settings.value.autoStartBreak) { + start() + } + } else { + currentPhase.value = 'work' + + if (settings.value.autoStartWork) { + start() + } + } + + resetTime() + } + + // 重置所有状态 + const reset = () => { + isRunning.value = false + currentPhase.value = 'work' + timeRemaining.value = settings.value.workDuration * 60 + sessionsCompleted.value = 0 + currentSession.value = 1 + } + + // tick (每秒调用) + const tick = () => { + if (!isRunning.value || timeRemaining.value <= 0) return + + timeRemaining.value-- + + if (timeRemaining.value <= 0) { + nextPhase() + } + } + + return { + enabled, + settings, + isRunning, + currentPhase, + timeRemaining, + sessionsCompleted, + currentSession, + totalTime, + progress, + formattedTime, + resetTime, + start, + pause, + toggle, + nextPhase, + reset, + tick, + } +})