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>
This commit is contained in:
211
WebUI/src/stores/useGoalStore.ts
Normal file
211
WebUI/src/stores/useGoalStore.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user