主要更新: - 🎯 新增综合分析仪表板,包含关键指标卡片、预算对比、智能洞察等组件 - 📊 增强数据可视化能力,新增标签云分析、时间维度分析等图表 - 📱 优化移动端响应式设计,改进触控交互体验 - 🔧 新增多个API模块(base、budget、tag),完善数据管理 - 🗂️ 重构路由结构,新增贷款、快速添加、设置、统计等独立模块 - 🔄 优化数据导入导出功能,增强数据迁移能力 - 🐛 修复多个已知问题,提升系统稳定性 技术改进: - 使用IndexedDB提升本地存储性能 - 实现模拟API服务,支持离线开发 - 增加自动化测试脚本,确保功能稳定 - 优化打包配置,提升构建效率 文件变更: - 新增42个文件 - 修改55个文件 - 包含测试脚本、配置文件、组件和API模块 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
256 lines
7.0 KiB
Vue
256 lines
7.0 KiB
Vue
<script setup lang="ts">
|
|
import type { FormInstance, Rule } from 'ant-design-vue';
|
|
|
|
import type { Budget } from '#/types/finance';
|
|
|
|
import { computed, ref, watch } from 'vue';
|
|
|
|
import {
|
|
Col,
|
|
Form,
|
|
FormItem,
|
|
InputNumber,
|
|
message,
|
|
Modal,
|
|
Row,
|
|
Select,
|
|
SelectOption,
|
|
} from 'ant-design-vue';
|
|
import dayjs from 'dayjs';
|
|
|
|
import { useBudgetStore } from '#/store/modules/budget';
|
|
import { useCategoryStore } from '#/store/modules/category';
|
|
|
|
interface Props {
|
|
visible: boolean;
|
|
budget?: Budget | null;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
visible: false,
|
|
budget: null,
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
success: [];
|
|
'update:visible': [value: boolean];
|
|
}>();
|
|
|
|
const budgetStore = useBudgetStore();
|
|
const categoryStore = useCategoryStore();
|
|
|
|
const formRef = ref<FormInstance>();
|
|
const formData = ref({
|
|
categoryId: '',
|
|
amount: 0,
|
|
currency: 'CNY',
|
|
period: 'monthly' as 'monthly' | 'yearly',
|
|
year: dayjs().year(),
|
|
month: dayjs().month() + 1,
|
|
});
|
|
|
|
const rules: Record<string, Rule[]> = {
|
|
categoryId: [{ required: true, message: '请选择分类' }],
|
|
amount: [
|
|
{ required: true, message: '请输入预算金额' },
|
|
{ type: 'number', min: 0.01, message: '预算金额必须大于0' },
|
|
],
|
|
currency: [{ required: true, message: '请选择货币' }],
|
|
period: [{ required: true, message: '请选择预算周期' }],
|
|
year: [{ required: true, message: '请选择年份' }],
|
|
month: [{ required: true, message: '请选择月份' }],
|
|
};
|
|
|
|
const title = computed(() => (props.budget ? '编辑预算' : '设置预算'));
|
|
|
|
const expenseCategories = computed(() =>
|
|
categoryStore.categories.filter((c) => c.type === 'expense'),
|
|
);
|
|
|
|
const yearOptions = computed(() => {
|
|
const currentYear = dayjs().year();
|
|
return Array.from({ length: 5 }, (_, i) => currentYear - 2 + i);
|
|
});
|
|
|
|
const visible = computed({
|
|
get: () => props.visible,
|
|
set: (val) => emit('update:visible', val),
|
|
});
|
|
|
|
const isCategoryBudgetExists = (categoryId: string) => {
|
|
if (props.budget && props.budget.categoryId === categoryId) {
|
|
return false;
|
|
}
|
|
return budgetStore.isBudgetExists(
|
|
categoryId,
|
|
formData.value.year,
|
|
formData.value.period,
|
|
formData.value.period === 'monthly' ? formData.value.month : undefined,
|
|
);
|
|
};
|
|
|
|
const handlePeriodChange = () => {
|
|
formData.value.month =
|
|
formData.value.period === 'yearly'
|
|
? (undefined as any)
|
|
: dayjs().month() + 1;
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
try {
|
|
await formRef.value?.validate();
|
|
|
|
const data = {
|
|
...formData.value,
|
|
month:
|
|
formData.value.period === 'monthly' ? formData.value.month : undefined,
|
|
};
|
|
|
|
if (props.budget) {
|
|
await budgetStore.updateBudget(props.budget.id, data);
|
|
message.success('预算更新成功');
|
|
} else {
|
|
await budgetStore.createBudget(data);
|
|
message.success('预算设置成功');
|
|
}
|
|
|
|
emit('success');
|
|
visible.value = false;
|
|
} catch (error) {
|
|
if (error !== 'Validation failed') {
|
|
message.error(props.budget ? '更新预算失败' : '设置预算失败');
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
formRef.value?.resetFields();
|
|
visible.value = false;
|
|
};
|
|
|
|
watch(
|
|
() => props.visible,
|
|
(newVal) => {
|
|
if (newVal) {
|
|
formData.value = props.budget
|
|
? {
|
|
categoryId: props.budget.categoryId,
|
|
amount: props.budget.amount,
|
|
currency: props.budget.currency,
|
|
period: props.budget.period,
|
|
year: props.budget.year,
|
|
month: props.budget.month || dayjs().month() + 1,
|
|
}
|
|
: {
|
|
categoryId: '',
|
|
amount: 0,
|
|
currency: 'CNY',
|
|
period: 'monthly',
|
|
year: dayjs().year(),
|
|
month: dayjs().month() + 1,
|
|
};
|
|
}
|
|
},
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="budget-setting">
|
|
<Modal
|
|
v-model:open="visible"
|
|
:title="title"
|
|
width="500px"
|
|
@ok="handleSubmit"
|
|
@cancel="handleCancel"
|
|
>
|
|
<Form ref="formRef" :model="formData" :rules="rules" layout="vertical">
|
|
<FormItem label="分类" name="categoryId">
|
|
<Select
|
|
v-model:value="formData.categoryId"
|
|
placeholder="选择分类"
|
|
:disabled="!!budget"
|
|
>
|
|
<SelectOption
|
|
v-for="category in expenseCategories"
|
|
:key="category.id"
|
|
:value="category.id"
|
|
:disabled="isCategoryBudgetExists(category.id)"
|
|
>
|
|
{{ category.icon }} {{ category.name }}
|
|
<span
|
|
v-if="isCategoryBudgetExists(category.id)"
|
|
style="color: #999"
|
|
>
|
|
(已设置预算)
|
|
</span>
|
|
</SelectOption>
|
|
</Select>
|
|
</FormItem>
|
|
|
|
<Row :gutter="16">
|
|
<Col :span="12">
|
|
<FormItem label="预算周期" name="period">
|
|
<Select
|
|
v-model:value="formData.period"
|
|
@change="handlePeriodChange"
|
|
>
|
|
<SelectOption value="monthly">月度预算</SelectOption>
|
|
<SelectOption value="yearly">年度预算</SelectOption>
|
|
</Select>
|
|
</FormItem>
|
|
</Col>
|
|
<Col :span="12">
|
|
<FormItem label="预算金额" name="amount">
|
|
<InputNumber
|
|
v-model:value="formData.amount"
|
|
:min="0"
|
|
:precision="2"
|
|
placeholder="输入预算金额"
|
|
style="width: 100%"
|
|
:formatter="
|
|
(value) => `¥ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
"
|
|
:parser="(value) => value.replace(/\¥\s?|(,*)/g, '')"
|
|
/>
|
|
</FormItem>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row :gutter="16">
|
|
<Col :span="12">
|
|
<FormItem label="年份" name="year">
|
|
<Select v-model:value="formData.year">
|
|
<SelectOption
|
|
v-for="year in yearOptions"
|
|
:key="year"
|
|
:value="year"
|
|
>
|
|
{{ year }}年
|
|
</SelectOption>
|
|
</Select>
|
|
</FormItem>
|
|
</Col>
|
|
<Col :span="12" v-if="formData.period === 'monthly'">
|
|
<FormItem label="月份" name="month">
|
|
<Select v-model:value="formData.month">
|
|
<SelectOption v-for="month in 12" :key="month" :value="month">
|
|
{{ month }}月
|
|
</SelectOption>
|
|
</Select>
|
|
</FormItem>
|
|
</Col>
|
|
</Row>
|
|
|
|
<FormItem label="货币" name="currency">
|
|
<Select v-model:value="formData.currency">
|
|
<SelectOption value="USD">USD ($)</SelectOption>
|
|
<SelectOption value="CNY">CNY (¥)</SelectOption>
|
|
<SelectOption value="THB">THB (฿)</SelectOption>
|
|
<SelectOption value="MMK">MMK (K)</SelectOption>
|
|
</Select>
|
|
</FormItem>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
</template>
|