release: Elysia ToDo v1.0.0
鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆 Made-with: Cursor
This commit is contained in:
208
WebUI/src/stores/useAccountStore.ts
Normal file
208
WebUI/src/stores/useAccountStore.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { accountApi } from '@/api/accounts'
|
||||
import type {
|
||||
FinancialAccount, AccountFormData, BalanceUpdateData,
|
||||
AccountHistoryRecord, DebtInstallment, DebtInstallmentFormData
|
||||
} from '@/api/types'
|
||||
|
||||
export const useAccountStore = defineStore('account', () => {
|
||||
const accounts = ref<FinancialAccount[]>([])
|
||||
const installments = ref<DebtInstallment[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const savingsAccounts = computed(() =>
|
||||
accounts.value.filter(a => a.account_type === 'savings' && a.is_active)
|
||||
)
|
||||
|
||||
const debtAccounts = computed(() =>
|
||||
accounts.value.filter(a => a.account_type === 'debt' && a.is_active)
|
||||
)
|
||||
|
||||
const totalSavings = computed(() =>
|
||||
savingsAccounts.value.reduce((sum, a) => sum + a.balance, 0)
|
||||
)
|
||||
|
||||
const totalDebt = computed(() =>
|
||||
debtAccounts.value.reduce((sum, a) => sum + a.balance, 0)
|
||||
)
|
||||
|
||||
const netAssets = computed(() => totalSavings.value - totalDebt.value)
|
||||
|
||||
const activeInstallments = computed(() =>
|
||||
installments.value.filter(i => !i.is_completed && i.days_until_payment !== null)
|
||||
)
|
||||
|
||||
const upcomingPayments = computed(() =>
|
||||
activeInstallments.value
|
||||
.filter(i => i.days_until_payment! >= 0)
|
||||
.sort((a, b) => a.days_until_payment! - b.days_until_payment!)
|
||||
)
|
||||
|
||||
// ============ 账户操作 ============
|
||||
|
||||
async function fetchAccounts() {
|
||||
loading.value = true
|
||||
try {
|
||||
accounts.value = await accountApi.getAccounts()
|
||||
} catch (error) {
|
||||
console.error('获取账户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createAccount(data: AccountFormData): Promise<FinancialAccount | null> {
|
||||
try {
|
||||
const account = await accountApi.createAccount(data)
|
||||
accounts.value.push(account)
|
||||
return account
|
||||
} catch (error) {
|
||||
console.error('创建账户失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAccount(id: number, data: Partial<AccountFormData>): Promise<FinancialAccount | null> {
|
||||
try {
|
||||
const updated = await accountApi.updateAccount(id, data)
|
||||
const index = accounts.value.findIndex(a => a.id === id)
|
||||
if (index !== -1) {
|
||||
accounts.value[index] = updated
|
||||
}
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('更新账户失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAccount(id: number): Promise<boolean> {
|
||||
try {
|
||||
await accountApi.deleteAccount(id)
|
||||
accounts.value = accounts.value.filter(a => a.id !== id)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除账户失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateBalance(id: number, data: BalanceUpdateData): Promise<FinancialAccount | null> {
|
||||
try {
|
||||
const updated = await accountApi.updateBalance(id, data)
|
||||
const index = accounts.value.findIndex(a => a.id === id)
|
||||
if (index !== -1) {
|
||||
accounts.value[index] = updated
|
||||
}
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('更新余额失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHistory(id: number, page = 1, pageSize = 20): Promise<AccountHistoryResponse> {
|
||||
try {
|
||||
return await accountApi.getHistory(id, { page, page_size: pageSize })
|
||||
} catch (error) {
|
||||
console.error('获取变更历史失败:', error)
|
||||
return { total: 0, page: 1, page_size: pageSize, records: [] }
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 分期计划操作 ============
|
||||
|
||||
async function fetchInstallments() {
|
||||
try {
|
||||
installments.value = await accountApi.getInstallments()
|
||||
} catch (error) {
|
||||
console.error('获取分期计划失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function createInstallment(data: DebtInstallmentFormData): Promise<DebtInstallment | null> {
|
||||
try {
|
||||
const inst = await accountApi.createInstallment(data)
|
||||
installments.value.push(inst)
|
||||
installments.value.sort((a, b) => {
|
||||
const aActive = !a.is_completed && a.days_until_payment !== null ? 0 : 1
|
||||
const bActive = !b.is_completed && b.days_until_payment !== null ? 0 : 1
|
||||
if (aActive !== bActive) return aActive - bActive
|
||||
return (a.days_until_payment ?? 9999) - (b.days_until_payment ?? 9999)
|
||||
})
|
||||
return inst
|
||||
} catch (error) {
|
||||
console.error('创建分期计划失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateInstallment(id: number, data: Partial<DebtInstallmentFormData>): Promise<DebtInstallment | null> {
|
||||
try {
|
||||
const updated = await accountApi.updateInstallment(id, data)
|
||||
const index = installments.value.findIndex(i => i.id === id)
|
||||
if (index !== -1) {
|
||||
installments.value[index] = updated
|
||||
}
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('更新分期计划失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteInstallment(id: number): Promise<boolean> {
|
||||
try {
|
||||
await accountApi.deleteInstallment(id)
|
||||
installments.value = installments.value.filter(i => i.id !== id)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除分期计划失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function payInstallment(id: number): Promise<DebtInstallment | null> {
|
||||
try {
|
||||
const updated = await accountApi.payInstallment(id)
|
||||
const index = installments.value.findIndex(i => i.id === id)
|
||||
if (index !== -1) {
|
||||
installments.value[index] = updated
|
||||
}
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('标记还款失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await Promise.all([fetchAccounts(), fetchInstallments()])
|
||||
}
|
||||
|
||||
return {
|
||||
accounts,
|
||||
installments,
|
||||
loading,
|
||||
savingsAccounts,
|
||||
debtAccounts,
|
||||
totalSavings,
|
||||
totalDebt,
|
||||
netAssets,
|
||||
activeInstallments,
|
||||
upcomingPayments,
|
||||
fetchAccounts,
|
||||
createAccount,
|
||||
updateAccount,
|
||||
deleteAccount,
|
||||
updateBalance,
|
||||
fetchHistory,
|
||||
fetchInstallments,
|
||||
createInstallment,
|
||||
updateInstallment,
|
||||
deleteInstallment,
|
||||
payInstallment,
|
||||
init,
|
||||
}
|
||||
})
|
||||
180
WebUI/src/stores/useAnniversaryStore.ts
Normal file
180
WebUI/src/stores/useAnniversaryStore.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { anniversaryApi } from '@/api/anniversaries'
|
||||
import type { Anniversary, AnniversaryFormData, AnniversaryCategory, AnniversaryCategoryFormData } from '@/api/types'
|
||||
|
||||
export const useAnniversaryStore = defineStore('anniversary', () => {
|
||||
const anniversaries = ref<Anniversary[]>([])
|
||||
const categories = ref<AnniversaryCategory[]>([])
|
||||
const loading = ref(false)
|
||||
const activeCategoryId = ref<number | null>(null)
|
||||
|
||||
const filteredAnniversaries = computed(() => {
|
||||
if (activeCategoryId.value === null) {
|
||||
return anniversaries.value
|
||||
}
|
||||
return anniversaries.value.filter(a => a.category_id === activeCategoryId.value)
|
||||
})
|
||||
|
||||
const upcomingAnniversaries = computed(() =>
|
||||
filteredAnniversaries.value.filter(a => a.days_until !== null && a.days_until! >= 0)
|
||||
)
|
||||
|
||||
const pastAnniversaries = computed(() =>
|
||||
filteredAnniversaries.value.filter(a => a.days_until === null || a.days_until! < 0)
|
||||
)
|
||||
|
||||
const todayAnniversaries = computed(() =>
|
||||
filteredAnniversaries.value.filter(a => a.days_until === 0)
|
||||
)
|
||||
|
||||
const remindAnniversaries = computed(() =>
|
||||
filteredAnniversaries.value.filter(a =>
|
||||
a.days_until !== null && a.days_until! >= 0 && a.days_until! <= a.remind_days_before
|
||||
)
|
||||
)
|
||||
|
||||
// ============ 纪念日操作 ============
|
||||
|
||||
async function fetchAnniversaries() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = activeCategoryId.value !== null
|
||||
? { category_id: activeCategoryId.value }
|
||||
: undefined
|
||||
anniversaries.value = await anniversaryApi.getAnniversaries(params)
|
||||
} catch (error) {
|
||||
console.error('获取纪念日列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createAnniversary(data: AnniversaryFormData): Promise<Anniversary | null> {
|
||||
try {
|
||||
const newAnniversary = await anniversaryApi.createAnniversary(data)
|
||||
anniversaries.value.unshift(newAnniversary)
|
||||
reSort()
|
||||
return newAnniversary
|
||||
} catch (error) {
|
||||
console.error('创建纪念日失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAnniversary(id: number, data: Partial<AnniversaryFormData>): Promise<Anniversary | null> {
|
||||
try {
|
||||
const updated = await anniversaryApi.updateAnniversary(id, data)
|
||||
const index = anniversaries.value.findIndex(a => a.id === id)
|
||||
if (index !== -1) {
|
||||
anniversaries.value[index] = updated
|
||||
}
|
||||
reSort()
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('更新纪念日失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAnniversary(id: number): Promise<boolean> {
|
||||
try {
|
||||
await anniversaryApi.deleteAnniversary(id)
|
||||
anniversaries.value = anniversaries.value.filter(a => a.id !== id)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除纪念日失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 分类操作 ============
|
||||
|
||||
async function fetchCategories() {
|
||||
try {
|
||||
categories.value = await anniversaryApi.getCategories()
|
||||
} catch (error) {
|
||||
console.error('获取纪念日分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function createCategory(data: AnniversaryCategoryFormData): Promise<AnniversaryCategory | null> {
|
||||
try {
|
||||
const newCat = await anniversaryApi.createCategory(data)
|
||||
categories.value.push(newCat)
|
||||
categories.value.sort((a, b) => a.sort_order - b.sort_order)
|
||||
return newCat
|
||||
} catch (error) {
|
||||
console.error('创建纪念日分类失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCategory(id: number, data: Partial<AnniversaryCategoryFormData>): Promise<AnniversaryCategory | null> {
|
||||
try {
|
||||
const updated = await anniversaryApi.updateCategory(id, data)
|
||||
const index = categories.value.findIndex(c => c.id === id)
|
||||
if (index !== -1) {
|
||||
categories.value[index] = updated
|
||||
}
|
||||
categories.value.sort((a, b) => a.sort_order - b.sort_order)
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('更新纪念日分类失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCategory(id: number): Promise<boolean> {
|
||||
try {
|
||||
await anniversaryApi.deleteCategory(id)
|
||||
categories.value = categories.value.filter(c => c.id !== id)
|
||||
if (activeCategoryId.value === id) {
|
||||
activeCategoryId.value = null
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除纪念日分类失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function setFilter(categoryId: number | null) {
|
||||
activeCategoryId.value = categoryId
|
||||
}
|
||||
|
||||
function reSort() {
|
||||
anniversaries.value.sort((a, b) => {
|
||||
const aUpcoming = a.days_until !== null && a.days_until >= 0 ? 0 : 1
|
||||
const bUpcoming = b.days_until !== null && b.days_until >= 0 ? 0 : 1
|
||||
if (aUpcoming !== bUpcoming) return aUpcoming - bUpcoming
|
||||
return (a.days_until ?? 9999) - (b.days_until ?? 9999)
|
||||
})
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await Promise.all([fetchAnniversaries(), fetchCategories()])
|
||||
}
|
||||
|
||||
return {
|
||||
anniversaries,
|
||||
categories,
|
||||
loading,
|
||||
activeCategoryId,
|
||||
filteredAnniversaries,
|
||||
upcomingAnniversaries,
|
||||
pastAnniversaries,
|
||||
todayAnniversaries,
|
||||
remindAnniversaries,
|
||||
fetchAnniversaries,
|
||||
createAnniversary,
|
||||
updateAnniversary,
|
||||
deleteAnniversary,
|
||||
fetchCategories,
|
||||
createCategory,
|
||||
updateCategory,
|
||||
deleteCategory,
|
||||
setFilter,
|
||||
init,
|
||||
}
|
||||
})
|
||||
65
WebUI/src/stores/useCategoryStore.ts
Normal file
65
WebUI/src/stores/useCategoryStore.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { categoryApi, type CategoryResponse } from '@/api/categories'
|
||||
import type { CategoryFormData } from '@/api/types'
|
||||
|
||||
export const useCategoryStore = defineStore('category', () => {
|
||||
const categories = ref<CategoryResponse[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchCategories() {
|
||||
loading.value = true
|
||||
try {
|
||||
categories.value = await categoryApi.getCategories()
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createCategory(data: CategoryFormData) {
|
||||
try {
|
||||
const newCategory = await categoryApi.createCategory(data)
|
||||
categories.value.push(newCategory)
|
||||
return newCategory
|
||||
} catch (error) {
|
||||
console.error('创建分类失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCategory(id: number, data: CategoryFormData) {
|
||||
try {
|
||||
const updatedCategory = await categoryApi.updateCategory(id, data)
|
||||
const index = categories.value.findIndex((c: CategoryResponse) => c.id === id)
|
||||
if (index !== -1) {
|
||||
categories.value[index] = updatedCategory
|
||||
}
|
||||
return updatedCategory
|
||||
} catch (error) {
|
||||
console.error('更新分类失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCategory(id: number) {
|
||||
try {
|
||||
await categoryApi.deleteCategory(id)
|
||||
categories.value = categories.value.filter((c: CategoryResponse) => c.id !== id)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
categories,
|
||||
loading,
|
||||
fetchCategories,
|
||||
createCategory,
|
||||
updateCategory,
|
||||
deleteCategory
|
||||
}
|
||||
})
|
||||
227
WebUI/src/stores/useHabitStore.ts
Normal file
227
WebUI/src/stores/useHabitStore.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { habitApi, habitGroupApi, type HabitResponse, type HabitGroupResponse, type HabitStatsResponse } from '@/api/habits'
|
||||
import type { HabitFormData, HabitGroupFormData } from '@/api/types'
|
||||
|
||||
export const useHabitStore = defineStore('habit', () => {
|
||||
const habits = ref<HabitResponse[]>([])
|
||||
const groups = ref<HabitGroupResponse[]>([])
|
||||
const statsMap = ref<Record<number, HabitStatsResponse>>({})
|
||||
const loading = ref(false)
|
||||
|
||||
// 按分组组织习惯
|
||||
const groupedHabits = computed(() => {
|
||||
const map = new Map<number, { group: HabitGroupResponse | null; habits: HabitResponse[] }>()
|
||||
|
||||
// 先添加有分组的
|
||||
for (const group of groups.value) {
|
||||
map.set(group.id, { group, habits: [] })
|
||||
}
|
||||
|
||||
for (const habit of habits.value) {
|
||||
if (habit.group_id && map.has(habit.group_id)) {
|
||||
map.get(habit.group_id)!.habits.push(habit)
|
||||
} else {
|
||||
// 未分组
|
||||
if (!map.has(0)) {
|
||||
map.set(0, { group: null, habits: [] })
|
||||
}
|
||||
map.get(0)!.habits.push(habit)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(map.values())
|
||||
})
|
||||
|
||||
// 今日统计
|
||||
const todaySummary = computed(() => {
|
||||
const todayHabits = habits.value.filter(h => !h.is_archived)
|
||||
const total = todayHabits.length
|
||||
let completed = 0
|
||||
let maxStreak = 0
|
||||
|
||||
for (const habit of todayHabits) {
|
||||
const stats = statsMap.value[habit.id]
|
||||
if (stats?.today_completed) completed++
|
||||
if (stats && stats.current_streak > maxStreak) maxStreak = stats.current_streak
|
||||
}
|
||||
|
||||
return { total, completed, maxStreak }
|
||||
})
|
||||
|
||||
async function fetchHabits(includeArchived = false) {
|
||||
loading.value = true
|
||||
try {
|
||||
habits.value = await habitApi.getHabits({ include_archived: includeArchived })
|
||||
} catch (error) {
|
||||
console.error('获取习惯列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGroups() {
|
||||
try {
|
||||
groups.value = await habitGroupApi.getGroups()
|
||||
} catch (error) {
|
||||
console.error('获取习惯分组失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStats(habitId: number) {
|
||||
try {
|
||||
statsMap.value[habitId] = await habitApi.getStats(habitId)
|
||||
} catch (error) {
|
||||
console.error('获取习惯统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAllStats() {
|
||||
for (const habit of habits.value) {
|
||||
await fetchStats(habit.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function createHabit(data: HabitFormData) {
|
||||
try {
|
||||
const newHabit = await habitApi.createHabit(data)
|
||||
habits.value.unshift(newHabit)
|
||||
await fetchStats(newHabit.id)
|
||||
return newHabit
|
||||
} catch (error) {
|
||||
console.error('创建习惯失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateHabit(id: number, data: Partial<HabitFormData>) {
|
||||
try {
|
||||
const updated = await habitApi.updateHabit(id, data)
|
||||
const index = habits.value.findIndex(h => h.id === id)
|
||||
if (index !== -1) habits.value[index] = updated
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('更新习惯失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHabit(id: number) {
|
||||
try {
|
||||
await habitApi.deleteHabit(id)
|
||||
habits.value = habits.value.filter(h => h.id !== id)
|
||||
delete statsMap.value[id]
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除习惯失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleArchive(id: number) {
|
||||
try {
|
||||
const updated = await habitApi.toggleArchive(id)
|
||||
const index = habits.value.findIndex(h => h.id === id)
|
||||
if (index !== -1) habits.value[index] = updated
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('切换归档状态失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function checkin(habitId: number, count?: number) {
|
||||
try {
|
||||
const result = await habitApi.checkin(habitId, count)
|
||||
await fetchStats(habitId)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('打卡失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelCheckin(habitId: number, count: number = 1) {
|
||||
try {
|
||||
await habitApi.cancelCheckin(habitId, count)
|
||||
await fetchStats(habitId)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('取消打卡失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function createGroup(data: HabitGroupFormData) {
|
||||
try {
|
||||
const newGroup = await habitGroupApi.createGroup(data)
|
||||
groups.value.push(newGroup)
|
||||
return newGroup
|
||||
} catch (error) {
|
||||
console.error('创建分组失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGroup(id: number, data: Partial<HabitGroupFormData>) {
|
||||
try {
|
||||
const updated = await habitGroupApi.updateGroup(id, data)
|
||||
const index = groups.value.findIndex(g => g.id === id)
|
||||
if (index !== -1) groups.value[index] = updated
|
||||
// 同步更新 habits 中的 group 引用
|
||||
for (const habit of habits.value) {
|
||||
if (habit.group_id === id) habit.group = updated
|
||||
}
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('更新分组失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteGroup(id: number) {
|
||||
try {
|
||||
await habitGroupApi.deleteGroup(id)
|
||||
groups.value = groups.value.filter(g => g.id !== id)
|
||||
// 清空关联习惯的 group_id
|
||||
for (const habit of habits.value) {
|
||||
if (habit.group_id === id) {
|
||||
habit.group_id = undefined
|
||||
habit.group = undefined
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除分组失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await Promise.all([fetchGroups(), fetchHabits()])
|
||||
await fetchAllStats()
|
||||
}
|
||||
|
||||
return {
|
||||
habits,
|
||||
groups,
|
||||
statsMap,
|
||||
loading,
|
||||
groupedHabits,
|
||||
todaySummary,
|
||||
fetchHabits,
|
||||
fetchGroups,
|
||||
fetchStats,
|
||||
fetchAllStats,
|
||||
createHabit,
|
||||
updateHabit,
|
||||
deleteHabit,
|
||||
toggleArchive,
|
||||
checkin,
|
||||
cancelCheckin,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
init
|
||||
}
|
||||
})
|
||||
50
WebUI/src/stores/useTagStore.ts
Normal file
50
WebUI/src/stores/useTagStore.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { tagApi, type TagResponse } from '@/api/tags'
|
||||
import type { TagFormData } from '@/api/types'
|
||||
|
||||
export const useTagStore = defineStore('tag', () => {
|
||||
const tags = ref<TagResponse[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchTags() {
|
||||
loading.value = true
|
||||
try {
|
||||
tags.value = await tagApi.getTags()
|
||||
} catch (error) {
|
||||
console.error('获取标签列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createTag(data: TagFormData) {
|
||||
try {
|
||||
const newTag = await tagApi.createTag(data)
|
||||
tags.value.push(newTag)
|
||||
return newTag
|
||||
} catch (error) {
|
||||
console.error('创建标签失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTag(id: number) {
|
||||
try {
|
||||
await tagApi.deleteTag(id)
|
||||
tags.value = tags.value.filter((t: TagResponse) => t.id !== id)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除标签失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tags,
|
||||
loading,
|
||||
fetchTags,
|
||||
createTag,
|
||||
deleteTag
|
||||
}
|
||||
})
|
||||
180
WebUI/src/stores/useTaskStore.ts
Normal file
180
WebUI/src/stores/useTaskStore.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { taskApi, type TaskResponse } from '@/api/tasks'
|
||||
import type { TaskFormData, TaskFilters } from '@/api/types'
|
||||
import { matchWithPinyin } from '@/utils/pinyin'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
export const useTaskStore = defineStore('task', () => {
|
||||
const allTasks = ref<TaskResponse[]>([])
|
||||
const loading = ref(false)
|
||||
const filters = ref<TaskFilters>({
|
||||
status: 'all',
|
||||
sort_by: 'priority',
|
||||
sort_order: 'desc'
|
||||
})
|
||||
|
||||
// 所有任务
|
||||
const tasks = computed(() => {
|
||||
let result = [...allTasks.value]
|
||||
|
||||
// 搜索过滤(支持拼音)
|
||||
if (filters.value.search?.trim()) {
|
||||
const keyword = filters.value.search.trim()
|
||||
result = result.filter(t =>
|
||||
matchWithPinyin(t.title, keyword) ||
|
||||
(t.description && matchWithPinyin(t.description, keyword)) ||
|
||||
t.tags?.some(tag => matchWithPinyin(tag.name, keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (filters.value.status === 'active') {
|
||||
result = result.filter(t => !t.is_completed)
|
||||
} else if (filters.value.status === 'completed') {
|
||||
result = result.filter(t => t.is_completed)
|
||||
}
|
||||
|
||||
// 分类筛选
|
||||
if (filters.value.category_id) {
|
||||
result = result.filter(t => t.category_id === filters.value.category_id)
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (filters.value.sort_by) {
|
||||
result.sort((a, b) => {
|
||||
let comparison = 0
|
||||
|
||||
if (filters.value.sort_by === 'priority') {
|
||||
const priorityOrder: Record<string, number> = { q1: 4, q2: 3, q3: 2, q4: 1 }
|
||||
const aOrder = priorityOrder[a.priority] || 0
|
||||
const bOrder = priorityOrder[b.priority] || 0
|
||||
comparison = aOrder - bOrder
|
||||
} else if (filters.value.sort_by === 'due_date') {
|
||||
if (!a.due_date && !b.due_date) comparison = 0
|
||||
else if (!a.due_date) comparison = 1
|
||||
else if (!b.due_date) comparison = -1
|
||||
else comparison = new Date(a.due_date).getTime() - new Date(b.due_date).getTime()
|
||||
} else {
|
||||
comparison = new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
}
|
||||
|
||||
return filters.value.sort_order === 'asc' ? -comparison : comparison
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 计算进行中和已完成的任务数量(基于所有任务)
|
||||
const activeTasks = computed(() => allTasks.value.filter(t => !t.is_completed))
|
||||
const completedTasks = computed(() => allTasks.value.filter(t => t.is_completed))
|
||||
const totalTasks = computed(() => allTasks.value)
|
||||
|
||||
// 按日期分组的任务(用于月视图)
|
||||
const tasksByDate = computed(() => {
|
||||
const grouped = new Map<string, TaskResponse[]>()
|
||||
const today = new Date()
|
||||
const todayStr = formatDate(today)
|
||||
|
||||
allTasks.value.forEach(task => {
|
||||
let dateKey: string
|
||||
if (task.due_date) {
|
||||
dateKey = formatDate(new Date(task.due_date))
|
||||
} else {
|
||||
// 无截止日期的任务显示在当天
|
||||
dateKey = todayStr
|
||||
}
|
||||
|
||||
if (!grouped.has(dateKey)) {
|
||||
grouped.set(dateKey, [])
|
||||
}
|
||||
grouped.get(dateKey)!.push(task)
|
||||
})
|
||||
|
||||
return grouped
|
||||
})
|
||||
|
||||
async function fetchTasks() {
|
||||
loading.value = true
|
||||
try {
|
||||
// 一次获取所有任务,在前端进行筛选和排序
|
||||
allTasks.value = await taskApi.getTasks({ status: 'all' })
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createTask(data: TaskFormData) {
|
||||
try {
|
||||
const newTask = await taskApi.createTask(data)
|
||||
allTasks.value.unshift(newTask)
|
||||
return newTask
|
||||
} catch (error) {
|
||||
console.error('创建任务失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTask(id: number, data: TaskFormData) {
|
||||
try {
|
||||
const updatedTask = await taskApi.updateTask(id, data)
|
||||
const index = allTasks.value.findIndex((t: TaskResponse) => t.id === id)
|
||||
if (index !== -1) {
|
||||
allTasks.value[index] = updatedTask
|
||||
}
|
||||
return updatedTask
|
||||
} catch (error) {
|
||||
console.error('更新任务失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask(id: number) {
|
||||
try {
|
||||
await taskApi.deleteTask(id)
|
||||
allTasks.value = allTasks.value.filter((t: TaskResponse) => t.id !== id)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除任务失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTask(id: number) {
|
||||
try {
|
||||
const updatedTask = await taskApi.toggleTask(id)
|
||||
const index = allTasks.value.findIndex((t: TaskResponse) => t.id === id)
|
||||
if (index !== -1) {
|
||||
allTasks.value[index] = updatedTask
|
||||
}
|
||||
return updatedTask
|
||||
} catch (error) {
|
||||
console.error('切换任务状态失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function setFilters(newFilters: TaskFilters) {
|
||||
filters.value = { ...filters.value, ...newFilters }
|
||||
// 筛选现在在前端完成,不需要重新请求
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
loading,
|
||||
filters,
|
||||
activeTasks,
|
||||
completedTasks,
|
||||
totalTasks: allTasks,
|
||||
tasksByDate,
|
||||
fetchTasks,
|
||||
createTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
toggleTask,
|
||||
setFilters
|
||||
}
|
||||
})
|
||||
72
WebUI/src/stores/useUIStore.ts
Normal file
72
WebUI/src/stores/useUIStore.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { Task, Category } from '@/api/types'
|
||||
|
||||
export const useUIStore = defineStore('ui', () => {
|
||||
const router = useRouter()
|
||||
|
||||
const taskDialogVisible = ref(false)
|
||||
const editingTask = ref<Task | null>(null)
|
||||
const categoryDialogVisible = ref(false)
|
||||
const editingCategory = ref<Category | null>(null)
|
||||
const sidebarCollapsed = ref(false)
|
||||
const globalLoading = ref(false)
|
||||
const currentView = ref<'list' | 'calendar' | 'quadrant' | 'profile' | 'settings' | 'habits' | 'anniversaries' | 'assets'>('list')
|
||||
const calendarMode = ref<'week' | 'monthly'>('monthly')
|
||||
|
||||
function openTaskDialog(task?: Task) {
|
||||
editingTask.value = task || null
|
||||
taskDialogVisible.value = true
|
||||
}
|
||||
|
||||
function closeTaskDialog() {
|
||||
taskDialogVisible.value = false
|
||||
editingTask.value = null
|
||||
}
|
||||
|
||||
function openCategoryDialog(category?: Category) {
|
||||
editingCategory.value = category || null
|
||||
categoryDialogVisible.value = true
|
||||
}
|
||||
|
||||
function closeCategoryDialog() {
|
||||
categoryDialogVisible.value = false
|
||||
editingCategory.value = null
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
function setLoading(loading: boolean) {
|
||||
globalLoading.value = loading
|
||||
}
|
||||
|
||||
function setCurrentView(view: 'list' | 'calendar' | 'quadrant' | 'profile' | 'settings' | 'habits' | 'anniversaries' | 'assets') {
|
||||
currentView.value = view
|
||||
}
|
||||
|
||||
function setCalendarMode(mode: 'week' | 'monthly') {
|
||||
calendarMode.value = mode
|
||||
}
|
||||
|
||||
return {
|
||||
taskDialogVisible,
|
||||
editingTask,
|
||||
categoryDialogVisible,
|
||||
editingCategory,
|
||||
sidebarCollapsed,
|
||||
globalLoading,
|
||||
currentView,
|
||||
calendarMode,
|
||||
openTaskDialog,
|
||||
closeTaskDialog,
|
||||
openCategoryDialog,
|
||||
closeCategoryDialog,
|
||||
toggleSidebar,
|
||||
setLoading,
|
||||
setCurrentView,
|
||||
setCalendarMode
|
||||
}
|
||||
})
|
||||
74
WebUI/src/stores/useUserSettingsStore.ts
Normal file
74
WebUI/src/stores/useUserSettingsStore.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { UserSettings, UserSettingsUpdate } from '@/api/types'
|
||||
import { getUserSettings, updateUserSettings as apiUpdateSettings } from '@/api/userSettings'
|
||||
|
||||
export const useUserSettingsStore = defineStore('userSettings', () => {
|
||||
const settings = ref<UserSettings | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchSettings() {
|
||||
loading.value = true
|
||||
try {
|
||||
settings.value = await getUserSettings()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSettings(data: UserSettingsUpdate) {
|
||||
loading.value = true
|
||||
try {
|
||||
settings.value = await apiUpdateSettings(data)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const nickname = ref('爱莉希雅')
|
||||
const avatar = ref('')
|
||||
const signature = ref('')
|
||||
const birthday = ref('')
|
||||
const email = ref('')
|
||||
const siteName = ref('爱莉希雅待办')
|
||||
const defaultView = ref('list')
|
||||
const defaultSortBy = ref('priority')
|
||||
const defaultSortOrder = ref('desc')
|
||||
|
||||
function syncFromSettings(s: UserSettings) {
|
||||
nickname.value = s.nickname || ''
|
||||
avatar.value = s.avatar || ''
|
||||
signature.value = s.signature || ''
|
||||
birthday.value = s.birthday || ''
|
||||
email.value = s.email || ''
|
||||
siteName.value = s.site_name || '爱莉希雅待办'
|
||||
defaultView.value = s.default_view || 'list'
|
||||
defaultSortBy.value = s.default_sort_by || 'priority'
|
||||
defaultSortOrder.value = s.default_sort_order || 'desc'
|
||||
}
|
||||
|
||||
async function fetchAndSync() {
|
||||
await fetchSettings()
|
||||
if (settings.value) {
|
||||
syncFromSettings(settings.value)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
settings,
|
||||
loading,
|
||||
fetchSettings,
|
||||
updateSettings,
|
||||
fetchAndSync,
|
||||
nickname,
|
||||
avatar,
|
||||
signature,
|
||||
birthday,
|
||||
email,
|
||||
siteName,
|
||||
defaultView,
|
||||
defaultSortBy,
|
||||
defaultSortOrder,
|
||||
syncFromSettings
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user