Files
ToDoList/WebUI/src/stores/useGoalStore.ts
祀梦 5af8cb5486 feat: add goal management module (long-term goals with phases, milestones, reviews)
Backend:
- Goal model: title, description, status (active/paused/completed/abandoned),
  progress (auto-computed from milestones), target_date, category, color, icon
- GoalStep model: unified phase/milestone with parent nesting
- GoalReview model: periodic reflection with rating
- goal_tasks M2M: link existing tasks to goals
- /api/goals CRUD + steps CRUD + reviews + task linking + status toggle
- Progress auto-calculated from milestone completion ratio

Frontend:
- GoalPage: card grid with progress bars, status filter
- GoalDetailPage: step tree (phases > milestones), reviews, linked tasks
- GoalDialog: create/edit form with color/icon picker
- Goal navigation in AppHeader
- useGoalStore: full Pinia store for all goal operations

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:34:39 +08:00

212 lines
6.0 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Goal, GoalDetail, GoalStep, GoalReview, GoalFormData, GoalStepFormData, GoalReviewFormData } from '@/api/types'
import * as goalApi from '@/api/goals'
export const useGoalStore = defineStore('goal', () => {
const goals = ref<Goal[]>([])
const currentGoal = ref<GoalDetail | null>(null)
const loading = ref(false)
const error = ref('')
const activeGoals = computed(() => goals.value.filter(g => g.status === 'active'))
const pausedGoals = computed(() => goals.value.filter(g => g.status === 'paused'))
const completedGoals = computed(() => goals.value.filter(g => g.status === 'completed'))
async function fetchGoals(status?: string) {
loading.value = true
error.value = ''
try {
goals.value = await goalApi.getGoals(status)
} catch (e: any) {
error.value = e?.response?.data?.detail || '获取目标列表失败'
} finally {
loading.value = false
}
}
async function fetchGoal(id: number) {
loading.value = true
error.value = ''
try {
currentGoal.value = await goalApi.getGoal(id)
} catch (e: any) {
error.value = e?.response?.data?.detail || '获取目标详情失败'
} finally {
loading.value = false
}
}
async function createGoal(data: GoalFormData): Promise<GoalDetail | null> {
loading.value = true
error.value = ''
try {
const goal = await goalApi.createGoal(data)
await fetchGoals()
return goal
} catch (e: any) {
error.value = e?.response?.data?.detail || '创建目标失败'
return null
} finally {
loading.value = false
}
}
async function updateGoal(id: number, data: Partial<GoalFormData>): Promise<GoalDetail | null> {
loading.value = true
error.value = ''
try {
const goal = await goalApi.updateGoal(id, data)
if (currentGoal.value?.id === id) {
currentGoal.value = { ...currentGoal.value, ...goal }
}
await fetchGoals()
return goal
} catch (e: any) {
error.value = e?.response?.data?.detail || '更新目标失败'
return null
} finally {
loading.value = false
}
}
async function deleteGoal(id: number): Promise<boolean> {
loading.value = true
error.value = ''
try {
await goalApi.deleteGoal(id)
if (currentGoal.value?.id === id) {
currentGoal.value = null
}
await fetchGoals()
return true
} catch (e: any) {
error.value = e?.response?.data?.detail || '删除目标失败'
return false
} finally {
loading.value = false
}
}
async function updateGoalStatus(id: number, status: string) {
try {
const goal = await goalApi.updateGoalStatus(id, status)
if (currentGoal.value?.id === id) {
currentGoal.value = { ...currentGoal.value, ...goal }
}
await fetchGoals()
} catch (e: any) {
error.value = e?.response?.data?.detail || '更新状态失败'
}
}
// ============ Steps ============
async function createStep(goalId: number, data: GoalStepFormData): Promise<GoalStep | null> {
try {
const step = await goalApi.createStep(goalId, data)
if (currentGoal.value?.id === goalId) {
await fetchGoal(goalId)
}
await fetchGoals()
return step
} catch (e: any) {
error.value = e?.response?.data?.detail || '添加步骤失败'
return null
}
}
async function updateStep(goalId: number, stepId: number, data: Partial<GoalStepFormData>) {
try {
await goalApi.updateStep(goalId, stepId, data)
if (currentGoal.value?.id === goalId) {
await fetchGoal(goalId)
}
await fetchGoals()
} catch (e: any) {
error.value = e?.response?.data?.detail || '更新步骤失败'
}
}
async function deleteStep(goalId: number, stepId: number) {
try {
await goalApi.deleteStep(goalId, stepId)
if (currentGoal.value?.id === goalId) {
await fetchGoal(goalId)
}
await fetchGoals()
} catch (e: any) {
error.value = e?.response?.data?.detail || '删除步骤失败'
}
}
async function toggleStep(goalId: number, stepId: number) {
try {
await goalApi.toggleStep(goalId, stepId)
if (currentGoal.value?.id === goalId) {
await fetchGoal(goalId)
}
await fetchGoals()
} catch (e: any) {
error.value = e?.response?.data?.detail || '切换步骤状态失败'
}
}
// ============ Reviews ============
async function createReview(goalId: number, data: GoalReviewFormData) {
try {
await goalApi.createReview(goalId, data)
if (currentGoal.value?.id === goalId) {
await fetchGoal(goalId)
}
} catch (e: any) {
error.value = e?.response?.data?.detail || '创建复盘失败'
}
}
async function deleteReview(goalId: number, reviewId: number) {
try {
await goalApi.deleteReview(goalId, reviewId)
if (currentGoal.value?.id === goalId) {
await fetchGoal(goalId)
}
} catch (e: any) {
error.value = e?.response?.data?.detail || '删除复盘失败'
}
}
// ============ Task Linking ============
async function linkTask(goalId: number, taskId: number) {
try {
await goalApi.linkTask(goalId, taskId)
if (currentGoal.value?.id === goalId) {
await fetchGoal(goalId)
}
} catch (e: any) {
error.value = e?.response?.data?.detail || '关联任务失败'
}
}
async function unlinkTask(goalId: number, taskId: number) {
try {
await goalApi.unlinkTask(goalId, taskId)
if (currentGoal.value?.id === goalId) {
await fetchGoal(goalId)
}
} catch (e: any) {
error.value = e?.response?.data?.detail || '取消关联失败'
}
}
return {
goals, currentGoal, loading, error,
activeGoals, pausedGoals, completedGoals,
fetchGoals, fetchGoal, createGoal, updateGoal, deleteGoal, updateGoalStatus,
createStep, updateStep, deleteStep, toggleStep,
createReview, deleteReview,
linkTask, unlinkTask,
}
})