feat: 初始化收支管理系统

This commit is contained in:
wangshuo
2026-08-30 14:36:22 +08:00
commit 733ed4bb86
79 changed files with 5808 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
<script setup lang="ts">
// 分类管理组件:展示收入/支出分类列表,支持新增、编辑、删除分类
import { ref, onMounted, computed, h } from 'vue'
import { useMessage, useDialog, NButton, NSpace, NDataTable, NModal, NForm, NFormItem, NInput, NInputNumber, NSelect } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { getCategoryList, createCategory, updateCategory, deleteCategory } from '../api/category'
const message = useMessage() // 消息提示
const dialog = useDialog() // 对话框
/** 分类数据结构 */
interface CategoryItem {
id: number
name: string
type: string // INCOME 或 EXPENSE
icon: string
sortOrder: number
createdAt: string
}
// 列表和弹窗状态
const categories = ref<CategoryItem[]>([]) // 全部分类列表
const loading = ref(false) // 列表加载状态
const showModal = ref(false) // 新增/编辑弹窗显示状态
const editingId = ref<number | null>(null) // 当前编辑的分类 ID(null 表示新增)
const form = ref({ name: '', type: 'EXPENSE', icon: '', sortOrder: 0 }) // 表单数据
// 类型选项
const typeOptions = [
{ label: '支出', value: 'EXPENSE' },
{ label: '收入', value: 'INCOME' },
]
// 按类型过滤分类列表
const incomeList = computed(() => categories.value.filter(c => c.type === 'INCOME')) // 收入分类
const expenseList = computed(() => categories.value.filter(c => c.type === 'EXPENSE')) // 支出分类
/** 创建表格列配置 */
function createColumns(): DataTableColumns<CategoryItem> {
return [
{ title: '名称', key: 'name' },
{ title: '图标', key: 'icon' },
{ title: '排序', key: 'sortOrder', width: 80 },
{
title: '操作', key: 'actions', width: 180,
render(row) {
// 编辑和删除按钮
return [
h(NSpace, null, {
default: () => [
h(NButton, { size: 'small', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
h(NButton, { size: 'small', type: 'error', onClick: () => handleDelete(row.id) }, { default: () => '删除' }),
],
}),
]
},
},
]
}
const columns = createColumns() // 表格列配置
/** 加载分类列表数据 */
async function fetchData() {
loading.value = true
try {
const res: any = await getCategoryList()
categories.value = res.data
} catch (e: any) {
message.error(e.message || '加载失败')
} finally {
loading.value = false
}
}
/** 打开新增分类弹窗,重置表单 */
function handleAdd() {
editingId.value = null
form.value = { name: '', type: 'EXPENSE', icon: '', sortOrder: 0 }
showModal.value = true
}
/** 打开编辑分类弹窗,填充表单数据 */
function handleEdit(row: CategoryItem) {
editingId.value = row.id
form.value = { name: row.name, type: row.type, icon: row.icon || '', sortOrder: row.sortOrder }
showModal.value = true
}
/** 提交表单:校验 -> 新增或更新 -> 刷新列表 */
async function handleSubmit() {
if (!form.value.name) {
message.warning('请输入分类名称')
return
}
try {
if (editingId.value) {
// 更新已有分类
await updateCategory(editingId.value, form.value)
message.success('修改成功')
} else {
// 创建新分类
await createCategory(form.value)
message.success('创建成功')
}
showModal.value = false
await fetchData() // 刷新列表
} catch (e: any) {
message.error(e.message || '操作失败')
}
}
/** 删除分类:弹出确认对话框,确认后执行删除并刷新列表 */
function handleDelete(id: number) {
dialog.warning({
title: '确认删除',
content: '确定要删除这个分类吗?',
positiveText: '删除',
negativeText: '取消',
onPositiveClick: async () => {
try {
await deleteCategory(id)
message.success('删除成功')
await fetchData()
} catch (e: any) {
message.error(e.message || '删除失败')
}
},
})
}
// 组件挂载时加载分类数据
onMounted(fetchData)
</script>
<template>
<div>
<!-- 顶部:标题和新增按钮 -->
<NSpace justify="space-between" style="margin-bottom: 16px;">
<h2 style="margin: 0;">分类管理</h2>
<NButton type="primary" @click="handleAdd">新增分类</NButton>
</NSpace>
<!-- 支出分类列表 -->
<h3>支出分类</h3>
<NDataTable :columns="columns" :data="expenseList" :loading="loading" :bordered="false" />
<!-- 收入分类列表 -->
<h3 style="margin-top: 24px;">收入分类</h3>
<NDataTable :columns="columns" :data="incomeList" :loading="loading" :bordered="false" />
<!-- 新增/编辑弹窗 -->
<NModal v-model:show="showModal" preset="card" :title="editingId ? '编辑分类' : '新增分类'" style="width: 450px;">
<NForm>
<NFormItem label="分类名称">
<NInput v-model:value="form.name" placeholder="请输入分类名称" />
</NFormItem>
<NFormItem label="类型">
<NSelect v-model:value="form.type" :options="typeOptions" />
</NFormItem>
<NFormItem label="图标">
<NInput v-model:value="form.icon" placeholder="可选,如 emoji" />
</NFormItem>
<NFormItem label="排序">
<NInputNumber v-model:value="form.sortOrder" :min="0" />
</NFormItem>
<NButton type="primary" block @click="handleSubmit">确定</NButton>
</NForm>
</NModal>
</div>
</template>