diff --git a/analytics-complete-success.png b/analytics-complete-success.png new file mode 100644 index 00000000..a56a68d1 Binary files /dev/null and b/analytics-complete-success.png differ diff --git a/analytics-debug.png b/analytics-debug.png new file mode 100644 index 00000000..a56a68d1 Binary files /dev/null and b/analytics-debug.png differ diff --git a/analytics-overview.png b/analytics-overview.png new file mode 100644 index 00000000..a56a68d1 Binary files /dev/null and b/analytics-overview.png differ diff --git a/analytics-success.png b/analytics-success.png new file mode 100644 index 00000000..a56a68d1 Binary files /dev/null and b/analytics-success.png differ diff --git a/apps/backend-mock/utils/mock-data.ts b/apps/backend-mock/utils/mock-data.ts index 192f30a0..e73189a2 100644 --- a/apps/backend-mock/utils/mock-data.ts +++ b/apps/backend-mock/utils/mock-data.ts @@ -59,22 +59,14 @@ const dashboardMenus = [ }, name: 'Dashboard', path: '/dashboard', - redirect: '/analytics', + redirect: '/workspace', children: [ - { - name: 'Analytics', - path: '/analytics', - component: '/dashboard/analytics/index', - meta: { - affixTab: true, - title: 'page.dashboard.analytics', - }, - }, { name: 'Workspace', path: '/workspace', component: '/dashboard/workspace/index', meta: { + affixTab: true, title: 'page.dashboard.workspace', }, }, @@ -82,6 +74,159 @@ const dashboardMenus = [ }, ]; +const analyticsMenus = [ + { + meta: { + order: 2, + title: '数据分析', + icon: 'ant-design:bar-chart-outlined', + }, + name: 'Analytics', + path: '/analytics', + redirect: '/analytics/overview', + children: [ + { + name: 'AnalyticsOverview', + path: '/analytics/overview', + component: '/analytics/overview/index', + meta: { + title: '数据概览', + icon: 'ant-design:dashboard-outlined', + }, + }, + { + name: 'AnalyticsTrends', + path: '/analytics/trends', + component: '/analytics/trends/index', + meta: { + title: '趋势分析', + icon: 'ant-design:line-chart-outlined', + }, + }, + { + name: 'AnalyticsReports', + path: '/analytics/reports', + meta: { + title: '报表', + icon: 'ant-design:file-text-outlined', + }, + children: [ + { + name: 'DailyReport', + path: '/analytics/reports/daily', + component: '/analytics/reports/daily', + meta: { + title: '日报表', + }, + }, + { + name: 'MonthlyReport', + path: '/analytics/reports/monthly', + component: '/analytics/reports/monthly', + meta: { + title: '月报表', + }, + }, + { + name: 'YearlyReport', + path: '/analytics/reports/yearly', + component: '/analytics/reports/yearly', + meta: { + title: '年报表', + }, + }, + { + name: 'CustomReport', + path: '/analytics/reports/custom', + component: '/analytics/reports/custom', + meta: { + title: '自定义报表', + }, + }, + ], + }, + ], + }, +]; + +const financeMenus = [ + { + meta: { + order: 3, + title: '财务管理', + icon: 'ant-design:dollar-circle-outlined', + }, + name: 'Finance', + path: '/finance', + redirect: '/finance/dashboard', + children: [ + { + name: 'FinanceDashboard', + path: '/finance/dashboard', + component: '/finance/dashboard/index', + meta: { + title: '财务仪表盘', + icon: 'ant-design:dashboard-outlined', + }, + }, + { + name: 'FinanceTransaction', + path: '/finance/transaction', + component: '/finance/transaction/index', + meta: { + title: '交易管理', + icon: 'ant-design:transaction-outlined', + }, + }, + { + name: 'FinanceCategory', + path: '/finance/category', + component: '/finance/category/index', + meta: { + title: '分类管理', + icon: 'ant-design:appstore-outlined', + }, + }, + { + name: 'FinancePerson', + path: '/finance/person', + component: '/finance/person/index', + meta: { + title: '人员管理', + icon: 'ant-design:user-outlined', + }, + }, + { + name: 'FinanceLoan', + path: '/finance/loan', + component: '/finance/loan/index', + meta: { + title: '贷款管理', + icon: 'ant-design:bank-outlined', + }, + }, + { + name: 'FinanceBudget', + path: '/finance/budget', + component: '/finance/budget/index', + meta: { + title: '预算管理', + icon: 'ant-design:wallet-outlined', + }, + }, + { + name: 'FinanceTag', + path: '/finance/tag', + component: '/finance/tag/index', + meta: { + title: '标签管理', + icon: 'ant-design:tags-outlined', + }, + }, + ], + }, +]; + const createDemosMenus = (role: 'admin' | 'super' | 'user') => { const roleWithMenus = { admin: { @@ -173,15 +318,15 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => { export const MOCK_MENUS = [ { - menus: [...dashboardMenus, ...createDemosMenus('super')], + menus: [...dashboardMenus, ...analyticsMenus, ...financeMenus, ...createDemosMenus('super')], username: 'vben', }, { - menus: [...dashboardMenus, ...createDemosMenus('admin')], + menus: [...dashboardMenus, ...analyticsMenus, ...financeMenus, ...createDemosMenus('admin')], username: 'admin', }, { - menus: [...dashboardMenus, ...createDemosMenus('user')], + menus: [...dashboardMenus, ...analyticsMenus, ...financeMenus, ...createDemosMenus('user')], username: 'jack', }, ]; diff --git a/apps/web-finance/README.md b/apps/web-finance/README.md index 99e4d8c6..a63e4b7f 100644 --- a/apps/web-finance/README.md +++ b/apps/web-finance/README.md @@ -5,12 +5,14 @@ ## 功能特性 ### 核心功能 + - **交易管理**:记录和管理所有收支交易,支持多币种、多状态管理 - **分类管理**:灵活的收支分类体系,支持自定义分类 - **人员管理**:管理交易相关人员,支持多角色(付款人、收款人、借款人、出借人) - **贷款管理**:完整的贷款和还款记录管理,自动计算还款进度 ### 技术特性 + - **现代化技术栈**:Vue 3 + TypeScript + Vite + Pinia + Ant Design Vue - **本地存储**:使用 IndexedDB 进行数据持久化,支持离线使用 - **Mock API**:完整的 Mock 数据服务,方便开发和测试 @@ -20,16 +22,19 @@ ## 快速开始 ### 安装依赖 + ```bash pnpm install ``` ### 启动开发服务器 + ```bash pnpm dev:finance ``` ### 访问系统 + - 开发地址:http://localhost:5666/ - 默认账号:vben - 默认密码:123456 @@ -58,17 +63,20 @@ src/ ## 数据存储 系统使用 IndexedDB 作为本地存储方案,支持: + - 自动数据持久化 - 事务支持 - 索引查询 - 数据备份和恢复 ### 数据迁移 + 如果您有旧版本的数据(存储在 localStorage),系统会在启动时自动检测并迁移到新的存储系统。 ## 开发指南 ### 添加新功能 + 1. 在 `types/finance.ts` 中定义数据类型 2. 在 `api/finance/` 中创建 API 接口 3. 在 `store/modules/` 中创建状态管理 @@ -76,11 +84,13 @@ src/ 5. 在 `router/routes/modules/` 中配置路由 ### Mock 数据 + Mock 数据服务位于 `api/mock/finance-service.ts`,可以根据需要修改初始数据或添加新的 Mock 接口。 ## 测试 运行 Playwright 测试: + ```bash node test-finance-system.js ``` @@ -88,6 +98,7 @@ node test-finance-system.js ## 部署 ### 构建生产版本 + ```bash pnpm build:finance ``` @@ -102,4 +113,4 @@ pnpm build:finance ## 许可证 -MIT \ No newline at end of file +MIT diff --git a/apps/web-finance/check-server.js b/apps/web-finance/check-server.js index c0b12712..afeaa7a9 100644 --- a/apps/web-finance/check-server.js +++ b/apps/web-finance/check-server.js @@ -2,58 +2,57 @@ import { chromium } from 'playwright'; (async () => { const browser = await chromium.launch({ - headless: false // 有头模式,方便观察 + headless: false, // 有头模式,方便观察 }); const context = await browser.newContext(); const page = await context.newPage(); // 监听控制台消息 - page.on('console', msg => { + page.on('console', (msg) => { console.log(`浏览器控制台 [${msg.type()}]:`, msg.text()); }); // 监听页面错误 - page.on('pageerror', error => { + page.on('pageerror', (error) => { console.error('页面错误:', error.message); }); try { console.log('正在访问 http://localhost:5666/ ...\n'); - + const response = await page.goto('http://localhost:5666/', { waitUntil: 'domcontentloaded', - timeout: 30000 + timeout: 30_000, }); - + console.log('响应状态:', response?.status()); console.log('当前URL:', page.url()); - + // 等待页面加载 await page.waitForTimeout(3000); - + // 截图查看页面状态 - await page.screenshot({ + await page.screenshot({ path: 'server-check.png', - fullPage: true + fullPage: true, }); console.log('\n已保存截图: server-check.png'); - + // 检查页面内容 const title = await page.title(); console.log('页面标题:', title); - + // 检查是否有错误信息 const bodyText = await page.locator('body').textContent(); console.log('\n页面内容预览:'); - console.log(bodyText.substring(0, 500) + '...'); - + console.log(`${bodyText.slice(0, 500)}...`); + // 保持浏览器打开10秒以便查看 console.log('\n浏览器将在10秒后关闭...'); - await page.waitForTimeout(10000); - + await page.waitForTimeout(10_000); } catch (error) { console.error('访问失败:', error.message); - + // 尝试获取更多错误信息 if (error.message.includes('ERR_CONNECTION_REFUSED')) { console.log('\n服务器可能未启动或端口错误'); @@ -62,4 +61,4 @@ import { chromium } from 'playwright'; } finally { await browser.close(); } -})(); \ No newline at end of file +})(); diff --git a/apps/web-finance/manual-check.js b/apps/web-finance/manual-check.js index d5b263de..517dc77d 100644 --- a/apps/web-finance/manual-check.js +++ b/apps/web-finance/manual-check.js @@ -3,17 +3,17 @@ import { chromium } from 'playwright'; (async () => { const browser = await chromium.launch({ headless: false, // 有头模式 - devtools: true // 打开开发者工具 + devtools: true, // 打开开发者工具 }); - + const context = await browser.newContext({ - viewport: { width: 1920, height: 1080 } + viewport: { width: 1920, height: 1080 }, }); - + const page = await context.newPage(); // 监听控制台消息 - page.on('console', msg => { + page.on('console', (msg) => { if (msg.type() === 'error') { console.log('❌ 控制台错误:', msg.text()); } else if (msg.type() === 'warning') { @@ -27,7 +27,7 @@ import { chromium } from 'playwright'; }); // 监听网络错误 - page.on('response', response => { + page.on('response', (response) => { if (response.status() >= 400) { console.log(`🚫 网络错误 [${response.status()}]: ${response.url()}`); } @@ -36,12 +36,12 @@ import { chromium } from 'playwright'; console.log('================================='); console.log('财务管理系统手动检查工具'); console.log('=================================\n'); - + console.log('正在打开系统...'); await page.goto('http://localhost:5666/', { - waitUntil: 'networkidle' + waitUntil: 'networkidle', }); - + console.log('\n请手动执行以下操作:'); console.log('1. 登录系统(用户名: vben, 密码: 123456)'); console.log('2. 逐个点击以下菜单并检查是否正常:'); @@ -57,17 +57,17 @@ import { chromium } from 'playwright'; console.log(' - 系统工具 > 数据备份'); console.log(' - 系统工具 > 预算管理'); console.log(' - 系统工具 > 标签管理'); - + console.log('\n需要检查的内容:'); console.log('✓ 页面是否正常加载'); console.log('✓ 是否有错误提示'); console.log('✓ 表格是否显示正常'); console.log('✓ 按钮是否可以点击'); console.log('✓ 图表是否正常显示(数据分析页面)'); - + console.log('\n控制台将实时显示错误信息...'); console.log('按 Ctrl+C 结束检查\n'); - + // 保持浏览器开启 await new Promise(() => {}); -})(); \ No newline at end of file +})(); diff --git a/apps/web-finance/quick-test.js b/apps/web-finance/quick-test.js index d9c39c90..b7a77b7d 100644 --- a/apps/web-finance/quick-test.js +++ b/apps/web-finance/quick-test.js @@ -2,7 +2,7 @@ import { chromium } from 'playwright'; (async () => { const browser = await chromium.launch({ - headless: false // 有头模式,方便观察 + headless: false, // 有头模式,方便观察 }); const context = await browser.newContext(); const page = await context.newPage(); @@ -13,14 +13,14 @@ import { chromium } from 'playwright'; // 直接访问交易管理页面 console.log('访问交易管理页面...'); await page.goto('http://localhost:5666/finance/transaction'); - + // 等待页面加载 await page.waitForTimeout(3000); - + // 截图 await page.screenshot({ path: 'transaction-page.png' }); console.log('页面截图已保存为 transaction-page.png'); - + // 测试导出CSV console.log('\n尝试导出CSV...'); try { @@ -28,25 +28,24 @@ import { chromium } from 'playwright'; if (await exportBtn.isVisible()) { await exportBtn.click(); await page.waitForTimeout(500); - + // 点击CSV导出 await page.locator('text="导出为CSV"').click(); console.log('CSV导出操作已触发'); } else { console.log('导出按钮未找到'); } - } catch (e) { + } catch { console.log('导出功能可能需要登录'); } - + console.log('\n测试完成!'); - } catch (error) { console.error('测试失败:', error.message); } - + // 保持浏览器打开20秒供查看 console.log('\n浏览器将在20秒后关闭...'); - await page.waitForTimeout(20000); + await page.waitForTimeout(20_000); await browser.close(); -})(); \ No newline at end of file +})(); diff --git a/apps/web-finance/src/api/finance/base.ts b/apps/web-finance/src/api/finance/base.ts new file mode 100644 index 00000000..8b1b763d --- /dev/null +++ b/apps/web-finance/src/api/finance/base.ts @@ -0,0 +1,18 @@ +// 基础API工厂函数 +export function createBaseApi(entity: string) { + return { + getList: async (params?: any) => { + // Mock实现 + return { items: [], total: 0 }; + }, + create: async (data: any) => { + return { ...data, id: Date.now().toString() }; + }, + update: async (id: string, data: any) => { + return { ...data, id }; + }, + delete: async (id: string) => { + return { success: true }; + }, + }; +} \ No newline at end of file diff --git a/apps/web-finance/src/api/finance/budget.ts b/apps/web-finance/src/api/finance/budget.ts new file mode 100644 index 00000000..f669acaf --- /dev/null +++ b/apps/web-finance/src/api/finance/budget.ts @@ -0,0 +1,58 @@ +import type { Budget } from '#/types/finance'; + +import { createBaseApi } from './base'; + +const baseBudgetApi = createBaseApi('budget'); + +export const budgetApi = { + ...baseBudgetApi, + + // 获取指定年月的预算列表 + getList: async (params?: { year?: number; month?: number; page?: number; pageSize?: number }) => { + // 模拟预算数据 + const mockBudgets: Budget[] = [ + { + id: '1', + categoryId: 'cat-1', + amount: 5000, + currency: 'CNY', + period: 'monthly', + year: params?.year || new Date().getFullYear(), + month: params?.month || new Date().getMonth() + 1, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + { + id: '2', + categoryId: 'cat-2', + amount: 3000, + currency: 'CNY', + period: 'monthly', + year: params?.year || new Date().getFullYear(), + month: params?.month || new Date().getMonth() + 1, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + { + id: '3', + categoryId: 'cat-3', + amount: 2000, + currency: 'CNY', + period: 'monthly', + year: params?.year || new Date().getFullYear(), + month: params?.month || new Date().getMonth() + 1, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + ]; + + return { + data: { + items: mockBudgets, + total: mockBudgets.length, + page: params?.page || 1, + pageSize: params?.pageSize || 10, + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/web-finance/src/api/finance/category.ts b/apps/web-finance/src/api/finance/category.ts index e862f788..428d11fa 100644 --- a/apps/web-finance/src/api/finance/category.ts +++ b/apps/web-finance/src/api/finance/category.ts @@ -1,4 +1,4 @@ -import type { Category, PageParams, PageResult } from '#/types/finance'; +import type { Category, PageParams } from '#/types/finance'; import { categoryService } from '#/api/mock/finance-service'; @@ -34,4 +34,4 @@ export async function deleteCategory(id: string) { // 获取分类树 export async function getCategoryTree() { return categoryService.getTree(); -} \ No newline at end of file +} diff --git a/apps/web-finance/src/api/finance/index.ts b/apps/web-finance/src/api/finance/index.ts index 4adb8b85..f9c1df0a 100644 --- a/apps/web-finance/src/api/finance/index.ts +++ b/apps/web-finance/src/api/finance/index.ts @@ -3,4 +3,12 @@ export * from './category'; export * from './loan'; export * from './person'; -export * from './transaction'; \ No newline at end of file +export * from './transaction'; +export * from './budget'; +export * from './tag'; + +// 分类统计 - 直接从Mock服务获取 +export async function getCategoryStatistics(params: any) { + const { getCategoryStatistics: getMockStatistics } = await import('#/api/mock/finance-service'); + return await getMockStatistics(params); +} diff --git a/apps/web-finance/src/api/finance/loan.ts b/apps/web-finance/src/api/finance/loan.ts index 069e68b1..6e3e10cf 100644 --- a/apps/web-finance/src/api/finance/loan.ts +++ b/apps/web-finance/src/api/finance/loan.ts @@ -1,9 +1,4 @@ -import type { - Loan, - LoanRepayment, - PageResult, - SearchParams -} from '#/types/finance'; +import type { Loan, LoanRepayment, SearchParams } from '#/types/finance'; import { loanService } from '#/api/mock/finance-service'; @@ -37,7 +32,10 @@ export async function deleteLoan(id: string) { } // 添加还款记录 -export async function addLoanRepayment(loanId: string, repayment: Partial) { +export async function addLoanRepayment( + loanId: string, + repayment: Partial, +) { return loanService.addRepayment(loanId, repayment); } @@ -49,4 +47,4 @@ export async function updateLoanStatus(id: string, status: Loan['status']) { // 获取贷款统计 export async function getLoanStatistics() { return loanService.getStatistics(); -} \ No newline at end of file +} diff --git a/apps/web-finance/src/api/finance/person.ts b/apps/web-finance/src/api/finance/person.ts index 606d83a4..2a436987 100644 --- a/apps/web-finance/src/api/finance/person.ts +++ b/apps/web-finance/src/api/finance/person.ts @@ -1,4 +1,4 @@ -import type { PageParams, PageResult, Person } from '#/types/finance'; +import type { PageParams, Person } from '#/types/finance'; import { personService } from '#/api/mock/finance-service'; @@ -34,4 +34,4 @@ export async function deletePerson(id: string) { // 搜索人员 export async function searchPersons(keyword: string) { return personService.search(keyword); -} \ No newline at end of file +} diff --git a/apps/web-finance/src/api/finance/tag.ts b/apps/web-finance/src/api/finance/tag.ts new file mode 100644 index 00000000..70300bfa --- /dev/null +++ b/apps/web-finance/src/api/finance/tag.ts @@ -0,0 +1,81 @@ +import type { Tag } from '#/types/finance'; + +import { createBaseApi } from './base'; + +const baseTagApi = createBaseApi('tag'); + +export const tagApi = { + ...baseTagApi, + + // 获取标签列表 + getList: async (params?: { page?: number; pageSize?: number }) => { + // 模拟标签数据 + const mockTags: Tag[] = [ + { + id: 'tag-1', + name: '日常开销', + color: '#5470c6', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + { + id: 'tag-2', + name: '餐饮', + color: '#91cc75', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + { + id: 'tag-3', + name: '交通', + color: '#fac858', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + { + id: 'tag-4', + name: '购物', + color: '#ee6666', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + { + id: 'tag-5', + name: '娱乐', + color: '#73c0de', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + { + id: 'tag-6', + name: '学习', + color: '#3ba272', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + { + id: 'tag-7', + name: '医疗', + color: '#fc8452', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + { + id: 'tag-8', + name: '投资', + color: '#9a60b4', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + ]; + + return { + data: { + items: mockTags, + total: mockTags.length, + page: params?.page || 1, + pageSize: params?.pageSize || 100, + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/web-finance/src/api/finance/transaction.ts b/apps/web-finance/src/api/finance/transaction.ts index c57d542c..4d7bd6ff 100644 --- a/apps/web-finance/src/api/finance/transaction.ts +++ b/apps/web-finance/src/api/finance/transaction.ts @@ -1,9 +1,8 @@ -import type { - ExportParams, - ImportResult, - PageResult, - SearchParams, - Transaction +import type { + ExportParams, + ImportResult, + SearchParams, + Transaction, } from '#/types/finance'; import { transactionService } from '#/api/mock/finance-service'; @@ -28,7 +27,10 @@ export async function createTransaction(data: Partial) { } // 更新交易 -export async function updateTransaction(id: string, data: Partial) { +export async function updateTransaction( + id: string, + data: Partial, +) { return transactionService.update(id, data); } @@ -61,4 +63,4 @@ export async function importTransactions(file: File) { // 获取统计数据 export async function getTransactionStatistics(params?: SearchParams) { return transactionService.getStatistics(params); -} \ No newline at end of file +} diff --git a/apps/web-finance/src/api/mock/finance-data.ts b/apps/web-finance/src/api/mock/finance-data.ts index c9c116b3..a1b6b328 100644 --- a/apps/web-finance/src/api/mock/finance-data.ts +++ b/apps/web-finance/src/api/mock/finance-data.ts @@ -1,14 +1,9 @@ // Mock 数据生成工具 -import type { - Category, - Loan, - Person, - Transaction -} from '#/types/finance'; +import type { Category, Loan, Person, Transaction } from '#/types/finance'; // 生成UUID function generateId(): string { - return Date.now().toString(36) + Math.random().toString(36).substr(2); + return Date.now().toString(36) + Math.random().toString(36).slice(2); } // 初始分类数据 @@ -19,7 +14,7 @@ export const mockCategories: Category[] = [ { id: '3', name: '兼职', type: 'income', created_at: '2024-01-01' }, { id: '4', name: '奖金', type: 'income', created_at: '2024-01-01' }, { id: '5', name: '其他收入', type: 'income', created_at: '2024-01-01' }, - + // 支出分类 { id: '6', name: '餐饮', type: 'expense', created_at: '2024-01-01' }, { id: '7', name: '交通', type: 'expense', created_at: '2024-01-01' }, @@ -73,52 +68,64 @@ export function generateMockTransactions(count: number = 50): Transaction[] { const currencies = ['USD', 'CNY', 'THB', 'MMK'] as const; const statuses = ['pending', 'completed', 'cancelled'] as const; const projects = ['项目A', '项目B', '项目C', '日常运营']; - + for (let i = 0; i < count; i++) { const type = Math.random() > 0.4 ? 'expense' : 'income'; - const categoryIds = type === 'income' ? ['1', '2', '3', '4', '5'] : ['6', '7', '8', '9', '10', '11', '12', '13']; + const categoryIds = + type === 'income' + ? ['1', '2', '3', '4', '5'] + : ['6', '7', '8', '9', '10', '11', '12', '13']; const date = new Date(); date.setDate(date.getDate() - Math.floor(Math.random() * 90)); // 最近90天的数据 - + transactions.push({ id: generateId(), - amount: Math.floor(Math.random() * 10000) + 100, + amount: Math.floor(Math.random() * 10_000) + 100, type, categoryId: categoryIds[Math.floor(Math.random() * categoryIds.length)], description: `${type === 'income' ? '收入' : '支出'}记录 ${i + 1}`, date: date.toISOString().split('T')[0], quantity: Math.floor(Math.random() * 10) + 1, project: projects[Math.floor(Math.random() * projects.length)], - payer: type === 'expense' ? '公司' : mockPersons[Math.floor(Math.random() * mockPersons.length)].name, - payee: type === 'income' ? '公司' : mockPersons[Math.floor(Math.random() * mockPersons.length)].name, + payer: + type === 'expense' + ? '公司' + : mockPersons[Math.floor(Math.random() * mockPersons.length)].name, + payee: + type === 'income' + ? '公司' + : mockPersons[Math.floor(Math.random() * mockPersons.length)].name, recorder: '管理员', currency: currencies[Math.floor(Math.random() * currencies.length)], status: statuses[Math.floor(Math.random() * statuses.length)], created_at: date.toISOString(), }); } - - return transactions.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + return transactions.sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), + ); } // 生成贷款数据 export function generateMockLoans(count: number = 10): Loan[] { const loans: Loan[] = []; const statuses = ['active', 'paid', 'overdue'] as const; - + for (let i = 0; i < count; i++) { const startDate = new Date(); startDate.setMonth(startDate.getMonth() - Math.floor(Math.random() * 12)); - + const dueDate = new Date(startDate); dueDate.setMonth(dueDate.getMonth() + Math.floor(Math.random() * 12) + 1); - + const status = statuses[Math.floor(Math.random() * statuses.length)]; - const amount = Math.floor(Math.random() * 100000) + 10000; - + const amount = Math.floor(Math.random() * 100_000) + 10_000; + const loan: Loan = { id: generateId(), - borrower: mockPersons[Math.floor(Math.random() * mockPersons.length)].name, + borrower: + mockPersons[Math.floor(Math.random() * mockPersons.length)].name, lender: mockPersons[Math.floor(Math.random() * mockPersons.length)].name, amount, currency: 'CNY', @@ -129,19 +136,19 @@ export function generateMockLoans(count: number = 10): Loan[] { repayments: [], created_at: startDate.toISOString(), }; - + // 生成还款记录 if (status !== 'active') { const repaymentCount = Math.floor(Math.random() * 5) + 1; let totalRepaid = 0; - + for (let j = 0; j < repaymentCount; j++) { const repaymentDate = new Date(startDate); repaymentDate.setMonth(repaymentDate.getMonth() + j + 1); - + const repaymentAmount = Math.floor(amount / repaymentCount); totalRepaid += repaymentAmount; - + loan.repayments.push({ id: generateId(), amount: repaymentAmount, @@ -150,7 +157,7 @@ export function generateMockLoans(count: number = 10): Loan[] { note: `第${j + 1}期还款`, }); } - + // 如果是已还清状态,确保还款总额等于贷款金额 if (status === 'paid' && totalRepaid < amount) { loan.repayments.push({ @@ -162,9 +169,9 @@ export function generateMockLoans(count: number = 10): Loan[] { }); } } - + loans.push(loan); } - + return loans; -} \ No newline at end of file +} diff --git a/apps/web-finance/src/api/mock/finance-service.ts b/apps/web-finance/src/api/mock/finance-service.ts index 94b03910..747bc232 100644 --- a/apps/web-finance/src/api/mock/finance-service.ts +++ b/apps/web-finance/src/api/mock/finance-service.ts @@ -1,64 +1,62 @@ // Mock API 服务实现 -import type { - Category, - ImportResult, - Loan, - LoanRepayment, - PageParams, - PageResult, - Person, - SearchParams, - Transaction +import type { + Category, + ImportResult, + Loan, + LoanRepayment, + PageParams, + PageResult, + Person, + SearchParams, + Transaction, } from '#/types/finance'; -import { - add, - addBatch, - clear, - get, - getAll, - getByIndex, - initDB, - remove, - STORES, - update +import { + add, + addBatch, + get, + getAll, + initDB, + remove, + STORES, + update, } from '#/utils/db'; -import { - generateMockLoans, - generateMockTransactions, - mockCategories, - mockPersons +import { + generateMockLoans, + generateMockTransactions, + mockCategories, + mockPersons, } from './finance-data'; // 生成UUID function generateId(): string { - return Date.now().toString(36) + Math.random().toString(36).substr(2); + return Date.now().toString(36) + Math.random().toString(36).slice(2); } // 初始化数据 export async function initializeData() { try { await initDB(); - + // 检查是否已有数据 const existingCategories = await getAll(STORES.CATEGORIES); if (existingCategories.length === 0) { console.log('初始化Mock数据...'); - + // 初始化分类 await addBatch(STORES.CATEGORIES, mockCategories); console.log('分类数据已初始化'); - + // 初始化人员 await addBatch(STORES.PERSONS, mockPersons); console.log('人员数据已初始化'); - + // 初始化交易 const transactions = generateMockTransactions(100); await addBatch(STORES.TRANSACTIONS, transactions); console.log('交易数据已初始化'); - + // 初始化贷款 const loans = generateMockLoans(20); await addBatch(STORES.LOANS, loans); @@ -75,22 +73,33 @@ export async function initializeData() { // 分页处理 function paginate(items: T[], params: PageParams): PageResult { const { page = 1, pageSize = 20, sortBy, sortOrder = 'desc' } = params; - + // 排序 - if (sortBy && (items[0] as any)[sortBy] !== undefined) { + if (sortBy && items.length > 0) { items.sort((a, b) => { const aVal = (a as any)[sortBy]; const bVal = (b as any)[sortBy]; + + // 处理日期字段的特殊排序 + if (sortBy === 'date' || sortBy === 'created_at' || sortBy === 'updated_at') { + const dateA = new Date(aVal).getTime(); + const dateB = new Date(bVal).getTime(); + return sortOrder === 'asc' ? dateA - dateB : dateB - dateA; + } + + // 处理其他字段 const order = sortOrder === 'asc' ? 1 : -1; + if (aVal === null || aVal === undefined) return order; + if (bVal === null || bVal === undefined) return -order; return aVal > bVal ? order : -order; }); } - + // 分页 const start = (page - 1) * pageSize; const end = start + pageSize; const paginatedItems = items.slice(start, end); - + return { items: paginatedItems, total: items.length, @@ -101,44 +110,111 @@ function paginate(items: T[], params: PageParams): PageResult { } // 搜索过滤 -function filterTransactions(transactions: Transaction[], params: SearchParams): Transaction[] { +function filterTransactions( + transactions: Transaction[], + params: SearchParams, +): Transaction[] { let filtered = transactions; - + if (params.keyword) { const keyword = params.keyword.toLowerCase(); - filtered = filtered.filter(t => - t.description?.toLowerCase().includes(keyword) || - t.project?.toLowerCase().includes(keyword) || - t.payer?.toLowerCase().includes(keyword) || - t.payee?.toLowerCase().includes(keyword) + filtered = filtered.filter( + (t) => + t.description?.toLowerCase().includes(keyword) || + t.project?.toLowerCase().includes(keyword) || + t.payer?.toLowerCase().includes(keyword) || + t.payee?.toLowerCase().includes(keyword), ); } - + if (params.type) { - filtered = filtered.filter(t => t.type === params.type); + filtered = filtered.filter((t) => t.type === params.type); } - + if (params.categoryId) { - filtered = filtered.filter(t => t.categoryId === params.categoryId); + filtered = filtered.filter((t) => t.categoryId === params.categoryId); } - + if (params.currency) { - filtered = filtered.filter(t => t.currency === params.currency); + filtered = filtered.filter((t) => t.currency === params.currency); } - + if (params.status) { - filtered = filtered.filter(t => t.status === params.status); + filtered = filtered.filter((t) => t.status === params.status); } + + if (params.dateFrom) { + filtered = filtered.filter((t) => t.date >= params.dateFrom); + } + + if (params.dateTo) { + filtered = filtered.filter((t) => t.date <= params.dateTo); + } + + return filtered; +} + +// 分类统计 +export async function getCategoryStatistics(params: any) { + const transactions = await getAll(STORES.TRANSACTIONS); + const categories = await getAll(STORES.CATEGORIES); + // 过滤日期范围 + let filtered = transactions; if (params.dateFrom) { filtered = filtered.filter(t => t.date >= params.dateFrom); } - if (params.dateTo) { filtered = filtered.filter(t => t.date <= params.dateTo); } - return filtered; + // 按分类统计 + const categoryStats: any[] = []; + let totalIncome = 0; + let totalExpense = 0; + + for (const category of categories) { + const categoryTransactions = filtered.filter(t => t.categoryId === category.id); + + if (categoryTransactions.length > 0) { + const amount = categoryTransactions.reduce((sum, t) => sum + t.amount, 0); + const count = categoryTransactions.length; + + if (category.type === 'income') { + totalIncome += amount; + } else { + totalExpense += amount; + } + + categoryStats.push({ + categoryId: category.id, + categoryName: category.name, + icon: category.icon || (category.type === 'income' ? '💰' : '💸'), + type: category.type, + amount, + count, + percentage: 0, // 稍后计算 + average: amount / count, + trend: Math.floor(Math.random() * 20) - 10, // 模拟趋势数据 + }); + } + } + + // 计算百分比 + categoryStats.forEach(stat => { + const total = stat.type === 'income' ? totalIncome : totalExpense; + stat.percentage = total > 0 ? Math.round((stat.amount / total) * 100) : 0; + }); + + // 按金额排序 + categoryStats.sort((a, b) => b.amount - a.amount); + + return { + categories, + totalIncome, + totalExpense, + categoryStats, + }; } // Category API @@ -147,11 +223,11 @@ export const categoryService = { const categories = await getAll(STORES.CATEGORIES); return paginate(categories, params || { page: 1, pageSize: 100 }); }, - + async getDetail(id: string): Promise { return get(STORES.CATEGORIES, id); }, - + async create(data: Partial): Promise { const category: Category = { id: generateId(), @@ -163,21 +239,25 @@ export const categoryService = { await add(STORES.CATEGORIES, category); return category; }, - + async update(id: string, data: Partial): Promise { const existing = await get(STORES.CATEGORIES, id); if (!existing) { throw new Error('Category not found'); } - const updated = { ...existing, ...data, updated_at: new Date().toISOString() }; + const updated = { + ...existing, + ...data, + updated_at: new Date().toISOString(), + }; await update(STORES.CATEGORIES, updated); return updated; }, - + async delete(id: string): Promise { await remove(STORES.CATEGORIES, id); }, - + async getTree(): Promise { const categories = await getAll(STORES.CATEGORIES); // 这里可以构建树形结构,暂时返回平铺数据 @@ -190,13 +270,19 @@ export const transactionService = { async getList(params: SearchParams): Promise> { const transactions = await getAll(STORES.TRANSACTIONS); const filtered = filterTransactions(transactions, params); - return paginate(filtered, params); + // 默认按日期倒序排序(最新的在前) + const sortParams = { + ...params, + sortBy: params.sortBy || 'date', + sortOrder: params.sortOrder || 'desc' + }; + return paginate(filtered, sortParams); }, - - async getDetail(id: string): Promise { + + async getDetail(id: string): Promise { return get(STORES.TRANSACTIONS, id); }, - + async create(data: Partial): Promise { const transaction: Transaction = { id: generateId(), @@ -218,39 +304,45 @@ export const transactionService = { await add(STORES.TRANSACTIONS, transaction); return transaction; }, - + async update(id: string, data: Partial): Promise { const existing = await get(STORES.TRANSACTIONS, id); if (!existing) { throw new Error('Transaction not found'); } - const updated = { ...existing, ...data, updated_at: new Date().toISOString() }; + const updated = { + ...existing, + ...data, + updated_at: new Date().toISOString(), + }; await update(STORES.TRANSACTIONS, updated); return updated; }, - + async delete(id: string): Promise { await remove(STORES.TRANSACTIONS, id); }, - + async batchDelete(ids: string[]): Promise { for (const id of ids) { await remove(STORES.TRANSACTIONS, id); } }, - + async getStatistics(params?: SearchParams): Promise { const transactions = await getAll(STORES.TRANSACTIONS); - const filtered = params ? filterTransactions(transactions, params) : transactions; - + const filtered = params + ? filterTransactions(transactions, params) + : transactions; + const totalIncome = filtered - .filter(t => t.type === 'income' && t.status === 'completed') + .filter((t) => t.type === 'income' && t.status === 'completed') .reduce((sum, t) => sum + t.amount, 0); - + const totalExpense = filtered - .filter(t => t.type === 'expense' && t.status === 'completed') + .filter((t) => t.type === 'expense' && t.status === 'completed') .reduce((sum, t) => sum + t.amount, 0); - + return { totalIncome, totalExpense, @@ -258,17 +350,17 @@ export const transactionService = { totalTransactions: filtered.length, }; }, - + async import(data: Transaction[]): Promise { const result: ImportResult = { success: 0, failed: 0, errors: [], }; - - for (let i = 0; i < data.length; i++) { + + for (const [i, datum] of data.entries()) { try { - await this.create(data[i]); + await this.create(datum); result.success++; } catch (error) { result.failed++; @@ -278,7 +370,7 @@ export const transactionService = { }); } } - + return result; }, }; @@ -289,11 +381,11 @@ export const personService = { const persons = await getAll(STORES.PERSONS); return paginate(persons, params || { page: 1, pageSize: 100 }); }, - - async getDetail(id: string): Promise { + + async getDetail(id: string): Promise { return get(STORES.PERSONS, id); }, - + async create(data: Partial): Promise { const person: Person = { id: generateId(), @@ -306,28 +398,33 @@ export const personService = { await add(STORES.PERSONS, person); return person; }, - + async update(id: string, data: Partial): Promise { const existing = await get(STORES.PERSONS, id); if (!existing) { throw new Error('Person not found'); } - const updated = { ...existing, ...data, updated_at: new Date().toISOString() }; + const updated = { + ...existing, + ...data, + updated_at: new Date().toISOString(), + }; await update(STORES.PERSONS, updated); return updated; }, - + async delete(id: string): Promise { await remove(STORES.PERSONS, id); }, - + async search(keyword: string): Promise { const persons = await getAll(STORES.PERSONS); const lowercaseKeyword = keyword.toLowerCase(); - return persons.filter(p => - p.name.toLowerCase().includes(lowercaseKeyword) || - p.contact?.toLowerCase().includes(lowercaseKeyword) || - p.description?.toLowerCase().includes(lowercaseKeyword) + return persons.filter( + (p) => + p.name.toLowerCase().includes(lowercaseKeyword) || + p.contact?.toLowerCase().includes(lowercaseKeyword) || + p.description?.toLowerCase().includes(lowercaseKeyword), ); }, }; @@ -337,27 +434,28 @@ export const loanService = { async getList(params: SearchParams): Promise> { const loans = await getAll(STORES.LOANS); let filtered = loans; - + if (params.status) { - filtered = filtered.filter(l => l.status === params.status); + filtered = filtered.filter((l) => l.status === params.status); } - + if (params.keyword) { const keyword = params.keyword.toLowerCase(); - filtered = filtered.filter(l => - l.borrower.toLowerCase().includes(keyword) || - l.lender.toLowerCase().includes(keyword) || - l.description?.toLowerCase().includes(keyword) + filtered = filtered.filter( + (l) => + l.borrower.toLowerCase().includes(keyword) || + l.lender.toLowerCase().includes(keyword) || + l.description?.toLowerCase().includes(keyword), ); } - + return paginate(filtered, params); }, - + async getDetail(id: string): Promise { return get(STORES.LOANS, id); }, - + async create(data: Partial): Promise { const loan: Loan = { id: generateId(), @@ -375,27 +473,34 @@ export const loanService = { await add(STORES.LOANS, loan); return loan; }, - + async update(id: string, data: Partial): Promise { const existing = await get(STORES.LOANS, id); if (!existing) { throw new Error('Loan not found'); } - const updated = { ...existing, ...data, updated_at: new Date().toISOString() }; + const updated = { + ...existing, + ...data, + updated_at: new Date().toISOString(), + }; await update(STORES.LOANS, updated); return updated; }, - + async delete(id: string): Promise { await remove(STORES.LOANS, id); }, - - async addRepayment(loanId: string, repayment: Partial): Promise { + + async addRepayment( + loanId: string, + repayment: Partial, + ): Promise { const loan = await get(STORES.LOANS, loanId); if (!loan) { throw new Error('Loan not found'); } - + const newRepayment: LoanRepayment = { id: generateId(), amount: repayment.amount!, @@ -403,19 +508,19 @@ export const loanService = { date: repayment.date || new Date().toISOString().split('T')[0], note: repayment.note, }; - + loan.repayments.push(newRepayment); - + // 检查是否已还清 const totalRepaid = loan.repayments.reduce((sum, r) => sum + r.amount, 0); if (totalRepaid >= loan.amount) { loan.status = 'paid'; } - + await update(STORES.LOANS, loan); return loan; }, - + async updateStatus(id: string, status: Loan['status']): Promise { const loan = await get(STORES.LOANS, id); if (!loan) { @@ -425,19 +530,21 @@ export const loanService = { await update(STORES.LOANS, loan); return loan; }, - + async getStatistics(): Promise { const loans = await getAll(STORES.LOANS); - - const activeLoans = loans.filter(l => l.status === 'active'); - const paidLoans = loans.filter(l => l.status === 'paid'); - const overdueLoans = loans.filter(l => l.status === 'overdue'); - + + const activeLoans = loans.filter((l) => l.status === 'active'); + const paidLoans = loans.filter((l) => l.status === 'paid'); + const overdueLoans = loans.filter((l) => l.status === 'overdue'); + const totalLent = loans.reduce((sum, l) => sum + l.amount, 0); - const totalRepaid = loans.reduce((sum, l) => - sum + l.repayments.reduce((repaySum, r) => repaySum + r.amount, 0), 0 + const totalRepaid = loans.reduce( + (sum, l) => + sum + l.repayments.reduce((repaySum, r) => repaySum + r.amount, 0), + 0, ); - + return { totalLent, totalBorrowed: totalLent, // 在实际应用中可能需要区分 @@ -447,4 +554,4 @@ export const loanService = { paidLoans: paidLoans.length, }; }, -}; \ No newline at end of file +}; diff --git a/apps/web-finance/src/api/mock/index.ts b/apps/web-finance/src/api/mock/index.ts new file mode 100644 index 00000000..1e42d884 --- /dev/null +++ b/apps/web-finance/src/api/mock/index.ts @@ -0,0 +1,5 @@ +// Mock API 注册 +import './finance-service'; + +// 导出服务 +export * from './finance-service'; \ No newline at end of file diff --git a/apps/web-finance/src/bootstrap.ts b/apps/web-finance/src/bootstrap.ts index 37b6a1bd..dd861216 100644 --- a/apps/web-finance/src/bootstrap.ts +++ b/apps/web-finance/src/bootstrap.ts @@ -6,7 +6,6 @@ import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; import '@vben/styles'; import '@vben/styles/antd'; -import '#/styles/mobile.css'; import { useTitle } from '@vueuse/core'; @@ -19,10 +18,12 @@ import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; +import '#/styles/mobile.css'; + async function bootstrap(namespace: string) { // 初始化数据库和 Mock 数据 await initializeData(); - + // 检查并执行数据迁移 if (needsMigration()) { console.log('检测到旧数据,开始迁移...'); diff --git a/apps/web-finance/src/components/charts/useChart.ts b/apps/web-finance/src/components/charts/useChart.ts index 4bf15c07..95c913e7 100644 --- a/apps/web-finance/src/components/charts/useChart.ts +++ b/apps/web-finance/src/components/charts/useChart.ts @@ -1,10 +1,10 @@ import type * as echarts from 'echarts'; + import type { Ref } from 'vue'; -import { computed, nextTick, onMounted, onUnmounted, ref, unref, watch } from 'vue'; +import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; import { useDebounceFn } from '@vueuse/core'; -import * as echartCore from 'echarts/core'; import { BarChart, LineChart, PieChart } from 'echarts/charts'; import { DataZoomComponent, @@ -14,6 +14,7 @@ import { ToolboxComponent, TooltipComponent, } from 'echarts/components'; +import * as echartCore from 'echarts/core'; import { LabelLayout, UniversalTransition } from 'echarts/features'; import { CanvasRenderer } from 'echarts/renderers'; @@ -37,7 +38,7 @@ export type EChartsOption = echarts.EChartsOption; export type EChartsInstance = echarts.ECharts; export interface UseChartOptions { - theme?: string | object; + theme?: object | string; initOptions?: echarts.EChartsCoreOption; loading?: boolean; loadingOptions?: object; @@ -47,7 +48,12 @@ export function useChart( elRef: Ref, options: UseChartOptions = {}, ) { - const { theme = 'light', initOptions = {}, loading = false, loadingOptions = {} } = options; + const { + theme = 'light', + initOptions = {}, + loading = false, + loadingOptions = {}, + } = options; let chartInstance: EChartsInstance | null = null; const cacheOptions = ref({}); @@ -116,15 +122,12 @@ export function useChart( ); // 监听元素变化,重新初始化 - watch( - elRef, - (el) => { - if (el) { - isDisposed.value = false; - setOptions(cacheOptions.value); - } - }, - ); + watch(elRef, (el) => { + if (el) { + isDisposed.value = false; + setOptions(cacheOptions.value); + } + }); // 挂载时初始化 onMounted(() => { @@ -144,4 +147,4 @@ export function useChart( resize, dispose, }; -} \ No newline at end of file +} diff --git a/apps/web-finance/src/locales/langs/zh-CN/analytics.json b/apps/web-finance/src/locales/langs/zh-CN/analytics.json index 6b235e0b..7f7c0645 100644 --- a/apps/web-finance/src/locales/langs/zh-CN/analytics.json +++ b/apps/web-finance/src/locales/langs/zh-CN/analytics.json @@ -7,20 +7,20 @@ "reports.monthly": "月报表", "reports.yearly": "年报表", "reports.custom": "自定义报表", - + "statistics.totalIncome": "总收入", "statistics.totalExpense": "总支出", "statistics.balance": "余额", "statistics.transactions": "交易数", "statistics.avgDaily": "日均", "statistics.avgMonthly": "月均", - + "chart.incomeExpense": "收支趋势", "chart.categoryDistribution": "分类分布", "chart.monthlyComparison": "月度对比", "chart.personAnalysis": "人员分析", "chart.projectAnalysis": "项目分析", - + "period.today": "今日", "period.yesterday": "昨日", "period.thisWeek": "本周", @@ -32,11 +32,11 @@ "period.thisYear": "今年", "period.lastYear": "去年", "period.custom": "自定义", - + "filter.dateRange": "日期范围", "filter.category": "分类", "filter.person": "人员", "filter.project": "项目", "filter.currency": "货币", "filter.type": "类型" -} \ No newline at end of file +} diff --git a/apps/web-finance/src/locales/langs/zh-CN/finance.json b/apps/web-finance/src/locales/langs/zh-CN/finance.json index 6bca7276..91d926b6 100644 --- a/apps/web-finance/src/locales/langs/zh-CN/finance.json +++ b/apps/web-finance/src/locales/langs/zh-CN/finance.json @@ -3,12 +3,13 @@ "dashboard": "仪表板", "transaction": "交易管理", "category": "分类管理", + "categoryStats": "分类统计", "person": "人员管理", "loan": "贷款管理", "tag": "标签管理", "budget": "预算管理", "mobile": "移动端", - + "transaction.list": "交易列表", "transaction.create": "新建交易", "transaction.edit": "编辑交易", @@ -16,7 +17,7 @@ "transaction.batchDelete": "批量删除", "transaction.export": "导出交易", "transaction.import": "导入交易", - + "transaction.amount": "金额", "transaction.type": "类型", "transaction.category": "分类", @@ -28,37 +29,37 @@ "transaction.recorder": "记录人", "transaction.currency": "货币", "transaction.status": "状态", - + "type.income": "收入", "type.expense": "支出", - + "status.pending": "待处理", "status.completed": "已完成", "status.cancelled": "已取消", - + "currency.USD": "美元", "currency.CNY": "人民币", "currency.THB": "泰铢", "currency.MMK": "缅元", - + "category.income": "收入分类", "category.expense": "支出分类", "category.create": "新建分类", "category.edit": "编辑分类", "category.delete": "删除分类", - + "person.list": "人员列表", "person.create": "新建人员", "person.edit": "编辑人员", "person.delete": "删除人员", "person.roles": "角色", "person.contact": "联系方式", - + "role.payer": "付款人", "role.payee": "收款人", "role.borrower": "借款人", "role.lender": "出借人", - + "loan.list": "贷款列表", "loan.create": "新建贷款", "loan.edit": "编辑贷款", @@ -69,11 +70,11 @@ "loan.dueDate": "到期日期", "loan.repayment": "还款记录", "loan.addRepayment": "添加还款", - + "loan.status.active": "进行中", "loan.status.paid": "已还清", "loan.status.overdue": "已逾期", - + "common.search": "搜索", "common.reset": "重置", "common.create": "新建", @@ -87,4 +88,4 @@ "common.actions": "操作", "common.loading": "加载中...", "common.noData": "暂无数据" -} \ No newline at end of file +} diff --git a/apps/web-finance/src/locales/langs/zh-CN/tools.json b/apps/web-finance/src/locales/langs/zh-CN/tools.json index c17b4b77..383c34b0 100644 --- a/apps/web-finance/src/locales/langs/zh-CN/tools.json +++ b/apps/web-finance/src/locales/langs/zh-CN/tools.json @@ -5,7 +5,7 @@ "backup": "数据备份", "budget": "预算管理", "tags": "标签管理", - + "import.title": "导入数据", "import.selectFile": "选择文件", "import.downloadTemplate": "下载模板", @@ -17,7 +17,7 @@ "import.result": "导入结果", "import.successCount": "成功条数", "import.failedCount": "失败条数", - + "export.title": "导出数据", "export.selectType": "选择类型", "export.selectFields": "选择字段", @@ -27,7 +27,7 @@ "export.pdf": "PDF文件", "export.dateRange": "日期范围", "export.filters": "筛选条件", - + "backup.title": "数据备份", "backup.create": "创建备份", "backup.restore": "恢复备份", @@ -37,7 +37,7 @@ "backup.manual": "手动备份", "backup.schedule": "备份计划", "backup.lastBackup": "最后备份", - + "budget.title": "预算管理", "budget.create": "创建预算", "budget.edit": "编辑预算", @@ -50,7 +50,7 @@ "budget.remaining": "剩余", "budget.progress": "执行进度", "budget.alert": "预警设置", - + "tags.title": "标签管理", "tags.create": "创建标签", "tags.edit": "编辑标签", @@ -59,4 +59,4 @@ "tags.color": "标签颜色", "tags.description": "标签描述", "tags.usage": "使用次数" -} \ No newline at end of file +} diff --git a/apps/web-finance/src/router/routes/modules/analytics.ts b/apps/web-finance/src/router/routes/modules/analytics.ts index 6634329a..04319900 100644 --- a/apps/web-finance/src/router/routes/modules/analytics.ts +++ b/apps/web-finance/src/router/routes/modules/analytics.ts @@ -78,4 +78,4 @@ const routes: RouteRecordRaw[] = [ }, ]; -export default routes; \ No newline at end of file +export default routes; diff --git a/apps/web-finance/src/router/routes/modules/finance.ts b/apps/web-finance/src/router/routes/modules/finance.ts deleted file mode 100644 index e96a8c3b..00000000 --- a/apps/web-finance/src/router/routes/modules/finance.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { RouteRecordRaw } from 'vue-router'; - -import { BasicLayout } from '#/layouts'; -import { $t } from '#/locales'; - -const routes: RouteRecordRaw[] = [ - { - component: BasicLayout, - meta: { - icon: 'ant-design:dollar-outlined', - order: 1, - title: $t('finance.title'), - }, - name: 'Finance', - path: '/finance', - children: [ - { - meta: { - icon: 'ant-design:home-outlined', - title: $t('finance.dashboard'), - }, - name: 'FinanceDashboard', - path: 'dashboard', - component: () => import('#/views/finance/dashboard/index.vue'), - }, - { - meta: { - icon: 'ant-design:swap-outlined', - title: $t('finance.transaction'), - }, - name: 'Transaction', - path: 'transaction', - component: () => import('#/views/finance/transaction/index.vue'), - }, - { - meta: { - icon: 'ant-design:appstore-outlined', - title: $t('finance.category'), - }, - name: 'Category', - path: 'category', - component: () => import('#/views/finance/category/index.vue'), - }, - { - meta: { - icon: 'ant-design:team-outlined', - title: $t('finance.person'), - }, - name: 'Person', - path: 'person', - component: () => import('#/views/finance/person/index.vue'), - }, - { - meta: { - icon: 'ant-design:bank-outlined', - title: $t('finance.loan'), - }, - name: 'Loan', - path: 'loan', - component: () => import('#/views/finance/loan/index.vue'), - }, - { - meta: { - icon: 'ant-design:tag-outlined', - title: $t('finance.tag'), - }, - name: 'Tag', - path: 'tag', - component: () => import('#/views/finance/tag/index.vue'), - }, - { - meta: { - icon: 'ant-design:wallet-outlined', - title: $t('finance.budget'), - }, - name: 'Budget', - path: 'budget', - component: () => import('#/views/finance/budget/index.vue'), - }, - { - meta: { - icon: 'ant-design:mobile-outlined', - title: $t('finance.mobile'), - hideInMenu: true, // 在桌面端菜单中隐藏 - }, - name: 'MobileFinance', - path: 'mobile', - component: () => import('#/views/finance/mobile/index.vue'), - }, - { - meta: { - icon: 'ant-design:bug-outlined', - title: 'API测试', - }, - name: 'TestAPI', - path: 'test-api', - component: () => import('#/views/finance/test-api.vue'), - }, - ], - }, -]; - -export default routes; \ No newline at end of file diff --git a/apps/web-finance/src/router/routes/modules/loan.ts b/apps/web-finance/src/router/routes/modules/loan.ts new file mode 100644 index 00000000..da1d6d2c --- /dev/null +++ b/apps/web-finance/src/router/routes/modules/loan.ts @@ -0,0 +1,29 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + hideChildrenInMenu: true, + icon: 'ant-design:bank-outlined', + order: 5, + title: '贷款管理', + }, + name: 'LoanManagement', + path: '/loan', + children: [ + { + name: 'LoanPage', + path: '', + component: () => import('#/views/finance/loan/index.vue'), + meta: { + title: '贷款管理', + }, + }, + ], + }, +]; + +export default routes; \ No newline at end of file diff --git a/apps/web-finance/src/router/routes/modules/quick-add.ts b/apps/web-finance/src/router/routes/modules/quick-add.ts new file mode 100644 index 00000000..5922b666 --- /dev/null +++ b/apps/web-finance/src/router/routes/modules/quick-add.ts @@ -0,0 +1,30 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'ant-design:plus-circle-outlined', + order: 1, + title: '记一笔', + }, + name: 'QuickAdd', + path: '/quick-add', + redirect: '/quick-add/index', + children: [ + { + name: 'QuickAddPage', + path: 'index', + component: () => import('#/views/finance/quick-add/index.vue'), + meta: { + hideInMenu: true, + title: '记一笔', + }, + }, + ], + }, +]; + +export default routes; \ No newline at end of file diff --git a/apps/web-finance/src/router/routes/modules/settings.ts b/apps/web-finance/src/router/routes/modules/settings.ts new file mode 100644 index 00000000..3aca7e15 --- /dev/null +++ b/apps/web-finance/src/router/routes/modules/settings.ts @@ -0,0 +1,56 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'ant-design:setting-outlined', + order: 4, + title: '设置', + }, + name: 'Settings', + path: '/settings', + children: [ + { + meta: { + icon: 'ant-design:appstore-outlined', + title: '分类管理', + }, + name: 'CategorySettings', + path: 'category', + component: () => import('#/views/finance/category/index.vue'), + }, + { + meta: { + icon: 'ant-design:wallet-outlined', + title: '预算设置', + }, + name: 'BudgetSettings', + path: 'budget', + component: () => import('#/views/finance/budget/index.vue'), + }, + { + meta: { + icon: 'ant-design:tag-outlined', + title: '标签管理', + }, + name: 'TagSettings', + path: 'tag', + component: () => import('#/views/finance/tag/index.vue'), + }, + { + meta: { + icon: 'ant-design:team-outlined', + title: '人员管理', + }, + name: 'PersonSettings', + path: 'person', + component: () => import('#/views/finance/person/index.vue'), + }, + ], + }, +]; + +export default routes; \ No newline at end of file diff --git a/apps/web-finance/src/router/routes/modules/statistics.ts b/apps/web-finance/src/router/routes/modules/statistics.ts new file mode 100644 index 00000000..fc56541c --- /dev/null +++ b/apps/web-finance/src/router/routes/modules/statistics.ts @@ -0,0 +1,56 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'ant-design:bar-chart-outlined', + order: 3, + title: '统计分析', + }, + name: 'Statistics', + path: '/statistics', + children: [ + { + meta: { + icon: 'ant-design:pie-chart-outlined', + title: '分类统计', + }, + name: 'CategoryStats', + path: 'category', + component: () => import('#/views/finance/category-stats/index.vue'), + }, + { + meta: { + icon: 'ant-design:line-chart-outlined', + title: '趋势分析', + }, + name: 'TrendAnalysis', + path: 'trend', + component: () => import('#/views/analytics/trends/index.vue'), + }, + { + meta: { + icon: 'ant-design:calendar-outlined', + title: '月度报表', + }, + name: 'MonthlyReport', + path: 'monthly', + component: () => import('#/views/analytics/reports/monthly.vue'), + }, + { + meta: { + icon: 'ant-design:fund-outlined', + title: '年度总结', + }, + name: 'YearlyReport', + path: 'yearly', + component: () => import('#/views/analytics/reports/yearly.vue'), + }, + ], + }, +]; + +export default routes; \ No newline at end of file diff --git a/apps/web-finance/src/router/routes/modules/tools.ts b/apps/web-finance/src/router/routes/modules/tools.ts index 83cbc0b5..f93fbac7 100644 --- a/apps/web-finance/src/router/routes/modules/tools.ts +++ b/apps/web-finance/src/router/routes/modules/tools.ts @@ -1,23 +1,22 @@ import type { RouteRecordRaw } from 'vue-router'; import { BasicLayout } from '#/layouts'; -import { $t } from '#/locales'; const routes: RouteRecordRaw[] = [ { component: BasicLayout, meta: { icon: 'ant-design:tool-outlined', - order: 3, - title: $t('tools.title'), + order: 6, + title: '系统工具', }, - name: 'Tools', + name: 'SystemTools', path: '/tools', children: [ { meta: { icon: 'ant-design:import-outlined', - title: $t('tools.import'), + title: '数据导入', }, name: 'DataImport', path: 'import', @@ -26,7 +25,7 @@ const routes: RouteRecordRaw[] = [ { meta: { icon: 'ant-design:export-outlined', - title: $t('tools.export'), + title: '数据导出', }, name: 'DataExport', path: 'export', @@ -34,33 +33,35 @@ const routes: RouteRecordRaw[] = [ }, { meta: { - icon: 'ant-design:database-outlined', - title: $t('tools.backup'), + icon: 'ant-design:cloud-download-outlined', + title: '备份恢复', }, - name: 'DataBackup', + name: 'BackupRestore', path: 'backup', component: () => import('#/views/tools/backup/index.vue'), }, { meta: { - icon: 'ant-design:calculator-outlined', - title: $t('tools.budget'), + icon: 'ant-design:mobile-outlined', + title: '移动版', + hideInMenu: true, }, - name: 'BudgetManagement', - path: 'budget', - component: () => import('#/views/tools/budget/index.vue'), + name: 'MobileFinance', + path: 'mobile', + component: () => import('#/views/finance/mobile/index.vue'), }, { meta: { - icon: 'ant-design:tags-outlined', - title: $t('tools.tags'), + icon: 'ant-design:bug-outlined', + title: 'API测试', + hideInMenu: true, }, - name: 'TagManagement', - path: 'tags', - component: () => import('#/views/tools/tags/index.vue'), + name: 'TestAPI', + path: 'test-api', + component: () => import('#/views/finance/test-api.vue'), }, ], }, ]; -export default routes; \ No newline at end of file +export default routes; diff --git a/apps/web-finance/src/router/routes/modules/transactions.ts b/apps/web-finance/src/router/routes/modules/transactions.ts new file mode 100644 index 00000000..ef55c00e --- /dev/null +++ b/apps/web-finance/src/router/routes/modules/transactions.ts @@ -0,0 +1,30 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'ant-design:unordered-list-outlined', + order: 2, + title: '交易记录', + }, + name: 'Transactions', + path: '/transactions', + redirect: '/transactions/list', + children: [ + { + name: 'TransactionsPage', + path: 'list', + component: () => import('#/views/finance/transaction/index.vue'), + meta: { + hideInMenu: true, + title: '交易记录', + }, + }, + ], + }, +]; + +export default routes; \ No newline at end of file diff --git a/apps/web-finance/src/store/modules/budget.ts b/apps/web-finance/src/store/modules/budget.ts index 9c3e1d4e..4786882c 100644 --- a/apps/web-finance/src/store/modules/budget.ts +++ b/apps/web-finance/src/store/modules/budget.ts @@ -3,7 +3,7 @@ import type { Budget, BudgetStats, Transaction } from '#/types/finance'; import dayjs from 'dayjs'; import { defineStore } from 'pinia'; -import { add, remove, getAll, update, STORES } from '#/utils/db'; +import { add, getAll, remove, STORES, update } from '#/utils/db'; interface BudgetState { budgets: Budget[]; @@ -22,23 +22,27 @@ export const useBudgetStore = defineStore('budget', { const now = dayjs(); const year = now.year(); const month = now.month() + 1; - - return state.budgets.filter(b => - b.year === year && - (b.period === 'yearly' || (b.period === 'monthly' && b.month === month)) + + return state.budgets.filter( + (b) => + b.year === year && + (b.period === 'yearly' || + (b.period === 'monthly' && b.month === month)), ); }, - + // 获取指定分类的当前预算 getCategoryBudget: (state) => (categoryId: string) => { const now = dayjs(); const year = now.year(); const month = now.month() + 1; - - return state.budgets.find(b => - b.categoryId === categoryId && - b.year === year && - (b.period === 'yearly' || (b.period === 'monthly' && b.month === month)) + + return state.budgets.find( + (b) => + b.categoryId === categoryId && + b.year === year && + (b.period === 'yearly' || + (b.period === 'monthly' && b.month === month)), ); }, }, @@ -71,7 +75,7 @@ export const useBudgetStore = defineStore('budget', { created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; - + await add(STORES.BUDGETS, newBudget); this.budgets.push(newBudget); return newBudget; @@ -84,15 +88,15 @@ export const useBudgetStore = defineStore('budget', { // 更新预算 async updateBudget(id: string, updates: Partial) { try { - const index = this.budgets.findIndex(b => b.id === id); + const index = this.budgets.findIndex((b) => b.id === id); if (index === -1) throw new Error('预算不存在'); - + const updatedBudget = { ...this.budgets[index], ...updates, updated_at: new Date().toISOString(), }; - + await update(STORES.BUDGETS, updatedBudget); this.budgets[index] = updatedBudget; return updatedBudget; @@ -106,8 +110,8 @@ export const useBudgetStore = defineStore('budget', { async deleteBudget(id: string) { try { await remove(STORES.BUDGETS, id); - const index = this.budgets.findIndex(b => b.id === id); - if (index > -1) { + const index = this.budgets.findIndex((b) => b.id === id); + if (index !== -1) { this.budgets.splice(index, 1); } } catch (error) { @@ -117,33 +121,40 @@ export const useBudgetStore = defineStore('budget', { }, // 计算预算统计 - calculateBudgetStats(budget: Budget, transactions: Transaction[]): BudgetStats { + calculateBudgetStats( + budget: Budget, + transactions: Transaction[], + ): BudgetStats { // 过滤出属于该预算期间的交易 let filteredTransactions: Transaction[] = []; - + if (budget.period === 'monthly') { - filteredTransactions = transactions.filter(t => { + filteredTransactions = transactions.filter((t) => { const date = dayjs(t.date); - return t.type === 'expense' && + return ( + t.type === 'expense' && t.categoryId === budget.categoryId && date.year() === budget.year && - date.month() + 1 === budget.month; + date.month() + 1 === budget.month + ); }); } else { // 年度预算 - filteredTransactions = transactions.filter(t => { + filteredTransactions = transactions.filter((t) => { const date = dayjs(t.date); - return t.type === 'expense' && + return ( + t.type === 'expense' && t.categoryId === budget.categoryId && - date.year() === budget.year; + date.year() === budget.year + ); }); } - + // 计算已花费金额 const spent = filteredTransactions.reduce((sum, t) => sum + t.amount, 0); const remaining = budget.amount - spent; const percentage = budget.amount > 0 ? (spent / budget.amount) * 100 : 0; - + return { budget, spent, @@ -154,13 +165,19 @@ export const useBudgetStore = defineStore('budget', { }, // 检查是否存在相同的预算 - isBudgetExists(categoryId: string, year: number, period: 'monthly' | 'yearly', month?: number): boolean { - return this.budgets.some(b => - b.categoryId === categoryId && - b.year === year && - b.period === period && - (period === 'yearly' || b.month === month) + isBudgetExists( + categoryId: string, + year: number, + period: 'monthly' | 'yearly', + month?: number, + ): boolean { + return this.budgets.some( + (b) => + b.categoryId === categoryId && + b.year === year && + b.period === period && + (period === 'yearly' || b.month === month), ); }, }, -}); \ No newline at end of file +}); diff --git a/apps/web-finance/src/store/modules/category.ts b/apps/web-finance/src/store/modules/category.ts index 31b9d825..ed69b9a2 100644 --- a/apps/web-finance/src/store/modules/category.ts +++ b/apps/web-finance/src/store/modules/category.ts @@ -4,10 +4,10 @@ import { computed, ref } from 'vue'; import { defineStore } from 'pinia'; -import { +import { createCategory as createCategoryApi, deleteCategory as deleteCategoryApi, - getCategoryList, + getCategoryList, getCategoryTree, updateCategory as updateCategoryApi, } from '#/api/finance'; @@ -90,4 +90,4 @@ export const useCategoryStore = defineStore('finance-category', () => { deleteCategory, getCategoryById, }; -}); \ No newline at end of file +}); diff --git a/apps/web-finance/src/store/modules/loan.ts b/apps/web-finance/src/store/modules/loan.ts index cdfc478b..c090714c 100644 --- a/apps/web-finance/src/store/modules/loan.ts +++ b/apps/web-finance/src/store/modules/loan.ts @@ -1,8 +1,8 @@ -import type { - Loan, - LoanRepayment, - LoanStatus, - SearchParams +import type { + Loan, + LoanRepayment, + LoanStatus, + SearchParams, } from '#/types/finance'; import { computed, ref } from 'vue'; @@ -87,7 +87,10 @@ export const useLoanStore = defineStore('finance-loan', () => { } // 添加还款记录 - async function addRepayment(loanId: string, repayment: Partial) { + async function addRepayment( + loanId: string, + repayment: Partial, + ) { const updatedLoan = await addRepaymentApi(loanId, repayment); const index = loans.value.findIndex((l) => l.id === loanId); if (index !== -1) { @@ -139,4 +142,4 @@ export const useLoanStore = defineStore('finance-loan', () => { getLoansByBorrower, getLoansByLender, }; -}); \ No newline at end of file +}); diff --git a/apps/web-finance/src/store/modules/person.ts b/apps/web-finance/src/store/modules/person.ts index a3936770..efc2186b 100644 --- a/apps/web-finance/src/store/modules/person.ts +++ b/apps/web-finance/src/store/modules/person.ts @@ -88,4 +88,4 @@ export const usePersonStore = defineStore('finance-person', () => { getPersonByName, getPersonsByRole, }; -}); \ No newline at end of file +}); diff --git a/apps/web-finance/src/store/modules/tag.ts b/apps/web-finance/src/store/modules/tag.ts index 5bc40f02..075bd7db 100644 --- a/apps/web-finance/src/store/modules/tag.ts +++ b/apps/web-finance/src/store/modules/tag.ts @@ -2,7 +2,7 @@ import type { Tag } from '#/types/finance'; import { defineStore } from 'pinia'; -import { add, remove, getAll, update, STORES } from '#/utils/db'; +import { add, getAll, remove, STORES, update } from '#/utils/db'; interface TagState { tags: Tag[]; @@ -20,10 +20,10 @@ export const useTagStore = defineStore('tag', { sortedTags: (state) => { return [...state.tags].sort((a, b) => a.name.localeCompare(b.name)); }, - + // 获取标签映射 tagMap: (state) => { - return new Map(state.tags.map(tag => [tag.id, tag])); + return new Map(state.tags.map((tag) => [tag.id, tag])); }, }, @@ -52,7 +52,7 @@ export const useTagStore = defineStore('tag', { created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; - + await add(STORES.TAGS, newTag); this.tags.push(newTag); return newTag; @@ -65,15 +65,15 @@ export const useTagStore = defineStore('tag', { // 更新标签 async updateTag(id: string, updates: Partial) { try { - const index = this.tags.findIndex(t => t.id === id); + const index = this.tags.findIndex((t) => t.id === id); if (index === -1) throw new Error('标签不存在'); - + const updatedTag = { ...this.tags[index], ...updates, updated_at: new Date().toISOString(), }; - + await update(STORES.TAGS, updatedTag); this.tags[index] = updatedTag; return updatedTag; @@ -87,8 +87,8 @@ export const useTagStore = defineStore('tag', { async deleteTag(id: string) { try { await remove(STORES.TAGS, id); - const index = this.tags.findIndex(t => t.id === id); - if (index > -1) { + const index = this.tags.findIndex((t) => t.id === id); + if (index !== -1) { this.tags.splice(index, 1); } } catch (error) { @@ -103,7 +103,7 @@ export const useTagStore = defineStore('tag', { for (const id of ids) { await remove(STORES.TAGS, id); } - this.tags = this.tags.filter(t => !ids.includes(t.id)); + this.tags = this.tags.filter((t) => !ids.includes(t.id)); } catch (error) { console.error('批量删除标签失败:', error); throw error; @@ -112,9 +112,7 @@ export const useTagStore = defineStore('tag', { // 检查标签名称是否已存在 isTagNameExists(name: string, excludeId?: string): boolean { - return this.tags.some(t => - t.name === name && t.id !== excludeId - ); + return this.tags.some((t) => t.name === name && t.id !== excludeId); }, }, -}); \ No newline at end of file +}); diff --git a/apps/web-finance/src/store/modules/transaction.ts b/apps/web-finance/src/store/modules/transaction.ts index a49d55a8..b9662afe 100644 --- a/apps/web-finance/src/store/modules/transaction.ts +++ b/apps/web-finance/src/store/modules/transaction.ts @@ -1,9 +1,8 @@ -import type { - ExportParams, - ImportResult, - PageResult, - SearchParams, - Transaction +import type { + ExportParams, + ImportResult, + SearchParams, + Transaction, } from '#/types/finance'; import { ref } from 'vue'; @@ -24,7 +23,7 @@ import { export const useTransactionStore = defineStore('finance-transaction', () => { // 状态 const transactions = ref([]); - const currentTransaction = ref(null); + const currentTransaction = ref(null); const loading = ref(false); const pageInfo = ref({ total: 0, @@ -66,7 +65,8 @@ export const useTransactionStore = defineStore('finance-transaction', () => { // 创建交易 async function createTransaction(data: Partial) { const newTransaction = await createTransactionApi(data); - transactions.value.unshift(newTransaction); + // 不在这里更新列表,让页面重新获取数据以确保排序正确 + // transactions.value.unshift(newTransaction); return newTransaction; } @@ -118,7 +118,7 @@ export const useTransactionStore = defineStore('finance-transaction', () => { } // 设置当前交易 - function setCurrentTransaction(transaction: Transaction | null) { + function setCurrentTransaction(transaction: null | Transaction) { currentTransaction.value = transaction; } @@ -138,4 +138,4 @@ export const useTransactionStore = defineStore('finance-transaction', () => { importTransactions, setCurrentTransaction, }; -}); \ No newline at end of file +}); diff --git a/apps/web-finance/src/styles/mobile.css b/apps/web-finance/src/styles/mobile.css index a26f97de..8c76de01 100644 --- a/apps/web-finance/src/styles/mobile.css +++ b/apps/web-finance/src/styles/mobile.css @@ -7,65 +7,65 @@ overscroll-behavior: none; -webkit-overflow-scrolling: touch; } - + /* 移除桌面端的侧边栏和顶部导航 */ .vben-layout-sidebar, .vben-layout-header { display: none !important; } - + /* 移动端内容区域全屏 */ .vben-layout-content { - margin: 0 !important; - padding: 0 !important; height: 100vh !important; + padding: 0 !important; + margin: 0 !important; } - + /* 优化点击效果 */ * { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; } - + /* 优化输入框 */ input, textarea, select { font-size: 16px !important; /* 防止iOS自动缩放 */ - -webkit-appearance: none; + appearance: none; } - + /* 优化按钮点击 */ button, .ant-btn { touch-action: manipulation; } - + /* 优化模态框和抽屉 */ .ant-modal { max-width: calc(100vw - 32px); } - + .ant-drawer-content-wrapper { border-top-left-radius: 12px; border-top-right-radius: 12px; } - + /* 优化表单项间距 */ .ant-form-item { margin-bottom: 16px; } - + /* 优化列表项 */ .ant-list-item { padding: 12px; } - + /* 优化卡片间距 */ .ant-card { margin-bottom: 12px; } - + /* 移动端安全区域适配 */ .mobile-finance, .mobile-quick-add, @@ -75,12 +75,12 @@ .mobile-more { padding-bottom: env(safe-area-inset-bottom); } - + /* 浮动按钮安全区域适配 */ .floating-button { bottom: calc(20px + env(safe-area-inset-bottom)) !important; } - + /* 底部标签栏安全区域适配 */ .mobile-tabs .ant-tabs-nav { padding-bottom: env(safe-area-inset-bottom); @@ -92,7 +92,7 @@ .mobile-quick-add .category-grid { grid-template-columns: repeat(5, 1fr); } - + .mobile-statistics .overview-cards { grid-template-columns: repeat(3, 1fr); } @@ -104,12 +104,12 @@ grid-template-columns: repeat(3, 1fr); gap: 8px; } - + .mobile-statistics .overview-cards { grid-template-columns: 1fr; gap: 8px; } - + .mobile-budget .budget-summary { flex-direction: column; text-align: center; @@ -120,10 +120,10 @@ @media (max-width: 768px) { /* 减少动画时间 */ * { - animation-duration: 0.2s !important; transition-duration: 0.2s !important; + animation-duration: 0.2s !important; } - + /* 禁用复杂动画 */ .ant-progress-circle { animation: none !important; @@ -139,7 +139,7 @@ .transaction-item { min-height: 44px; } - + /* 增大关闭按钮 */ .ant-modal-close, .ant-drawer-close { @@ -147,4 +147,4 @@ height: 44px; line-height: 44px; } -} \ No newline at end of file +} diff --git a/apps/web-finance/src/types/finance.ts b/apps/web-finance/src/types/finance.ts index 9bfe095a..9f287213 100644 --- a/apps/web-finance/src/types/finance.ts +++ b/apps/web-finance/src/types/finance.ts @@ -1,19 +1,19 @@ // 财务管理系统类型定义 // 货币类型 -export type Currency = 'USD' | 'CNY' | 'THB' | 'MMK'; +export type Currency = 'CNY' | 'MMK' | 'THB' | 'USD'; // 交易类型 -export type TransactionType = 'income' | 'expense'; +export type TransactionType = 'expense' | 'income'; // 人员角色 -export type PersonRole = 'payer' | 'payee' | 'borrower' | 'lender'; +export type PersonRole = 'borrower' | 'lender' | 'payee' | 'payer'; // 贷款状态 -export type LoanStatus = 'active' | 'paid' | 'overdue'; +export type LoanStatus = 'active' | 'overdue' | 'paid'; // 交易状态 -export type TransactionStatus = 'pending' | 'completed' | 'cancelled'; +export type TransactionStatus = 'cancelled' | 'completed' | 'pending'; // 分类 export interface Category { @@ -91,8 +91,8 @@ export interface Statistics { balance: number; currency: Currency; period?: { - start: string; end: string; + start: string; }; } @@ -122,7 +122,7 @@ export interface SearchParams extends PageParams { currency?: Currency; dateFrom?: string; dateTo?: string; - status?: TransactionStatus | LoanStatus; + status?: LoanStatus | TransactionStatus; } // 导入结果 @@ -130,14 +130,14 @@ export interface ImportResult { success: number; failed: number; errors: Array<{ - row: number; message: string; + row: number; }>; } // 导出参数 export interface ExportParams { - format: 'excel' | 'csv' | 'pdf'; + format: 'csv' | 'excel' | 'pdf'; fields?: string[]; filters?: SearchParams; } @@ -172,4 +172,4 @@ export interface BudgetStats { remaining: number; percentage: number; transactions: number; -} \ No newline at end of file +} diff --git a/apps/web-finance/src/utils/data-migration.ts b/apps/web-finance/src/utils/data-migration.ts index 254dc301..92e33bb3 100644 --- a/apps/web-finance/src/utils/data-migration.ts +++ b/apps/web-finance/src/utils/data-migration.ts @@ -1,10 +1,5 @@ // 数据迁移工具 - 从旧的 localStorage 迁移到 IndexedDB -import type { - Category, - Loan, - Person, - Transaction -} from '#/types/finance'; +import type { Category, Loan, Person, Transaction } from '#/types/finance'; import { importDatabase } from './db'; @@ -18,12 +13,12 @@ const OLD_STORAGE_KEYS = { // 生成新的 ID function generateNewId(): string { - return Date.now().toString(36) + Math.random().toString(36).substr(2); + return Date.now().toString(36) + Math.random().toString(36).slice(2); } // 迁移分类数据 function migrateCategories(oldCategories: any[]): Category[] { - return oldCategories.map(cat => ({ + return oldCategories.map((cat) => ({ id: cat.id || generateNewId(), name: cat.name, type: cat.type, @@ -34,7 +29,7 @@ function migrateCategories(oldCategories: any[]): Category[] { // 迁移人员数据 function migratePersons(oldPersons: any[]): Person[] { - return oldPersons.map(person => ({ + return oldPersons.map((person) => ({ id: person.id || generateNewId(), name: person.name, roles: person.roles || [], @@ -46,7 +41,7 @@ function migratePersons(oldPersons: any[]): Person[] { // 迁移交易数据 function migrateTransactions(oldTransactions: any[]): Transaction[] { - return oldTransactions.map(trans => ({ + return oldTransactions.map((trans) => ({ id: trans.id || generateNewId(), amount: Number(trans.amount) || 0, type: trans.type, @@ -66,7 +61,7 @@ function migrateTransactions(oldTransactions: any[]): Transaction[] { // 迁移贷款数据 function migrateLoans(oldLoans: any[]): Loan[] { - return oldLoans.map(loan => ({ + return oldLoans.map((loan) => ({ id: loan.id || generateNewId(), borrower: loan.borrower, lender: loan.lender, @@ -94,26 +89,26 @@ function readOldData(key: string): T[] { // 执行数据迁移 export async function migrateData(): Promise<{ - success: boolean; - message: string; details?: any; + message: string; + success: boolean; }> { try { console.log('开始数据迁移...'); - + // 读取旧数据 const oldCategories = readOldData(OLD_STORAGE_KEYS.CATEGORIES); const oldPersons = readOldData(OLD_STORAGE_KEYS.PERSONS); const oldTransactions = readOldData(OLD_STORAGE_KEYS.TRANSACTIONS); const oldLoans = readOldData(OLD_STORAGE_KEYS.LOANS); - + console.log('读取到的旧数据:', { categories: oldCategories.length, persons: oldPersons.length, transactions: oldTransactions.length, loans: oldLoans.length, }); - + // 如果没有旧数据,则不需要迁移 if ( oldCategories.length === 0 && @@ -126,13 +121,13 @@ export async function migrateData(): Promise<{ message: '没有需要迁移的数据', }; } - + // 转换数据格式 const categories = migrateCategories(oldCategories); const persons = migratePersons(oldPersons); const transactions = migrateTransactions(oldTransactions); const loans = migrateLoans(oldLoans); - + // 导入到新系统 await importDatabase({ categories, @@ -140,13 +135,13 @@ export async function migrateData(): Promise<{ transactions, loans, }); - + // 迁移成功后,可以选择清除旧数据 // localStorage.removeItem(OLD_STORAGE_KEYS.CATEGORIES); // localStorage.removeItem(OLD_STORAGE_KEYS.PERSONS); // localStorage.removeItem(OLD_STORAGE_KEYS.TRANSACTIONS); // localStorage.removeItem(OLD_STORAGE_KEYS.LOANS); - + return { success: true, message: '数据迁移成功', @@ -169,11 +164,11 @@ export async function migrateData(): Promise<{ // 检查是否需要迁移 export function needsMigration(): boolean { - const hasOldData = + const hasOldData = localStorage.getItem(OLD_STORAGE_KEYS.CATEGORIES) || localStorage.getItem(OLD_STORAGE_KEYS.PERSONS) || localStorage.getItem(OLD_STORAGE_KEYS.TRANSACTIONS) || localStorage.getItem(OLD_STORAGE_KEYS.LOANS); - + return !!hasOldData; -} \ No newline at end of file +} diff --git a/apps/web-finance/src/utils/db.ts b/apps/web-finance/src/utils/db.ts index 0f3b0aa3..815e5c55 100644 --- a/apps/web-finance/src/utils/db.ts +++ b/apps/web-finance/src/utils/db.ts @@ -1,10 +1,5 @@ // IndexedDB 工具类 -import type { - Category, - Loan, - Person, - Transaction -} from '#/types/finance'; +import type { Category, Loan, Person, Transaction } from '#/types/finance'; const DB_NAME = 'TokenRecordsDB'; const DB_VERSION = 2; // 升级版本号以添加新表 @@ -46,11 +41,16 @@ export function initDB(): Promise { // 创建交易表 if (!database.objectStoreNames.contains(STORES.TRANSACTIONS)) { - const transactionStore = database.createObjectStore(STORES.TRANSACTIONS, { - keyPath: 'id', - }); + const transactionStore = database.createObjectStore( + STORES.TRANSACTIONS, + { + keyPath: 'id', + }, + ); transactionStore.createIndex('type', 'type', { unique: false }); - transactionStore.createIndex('categoryId', 'categoryId', { unique: false }); + transactionStore.createIndex('categoryId', 'categoryId', { + unique: false, + }); transactionStore.createIndex('date', 'date', { unique: false }); transactionStore.createIndex('currency', 'currency', { unique: false }); transactionStore.createIndex('status', 'status', { unique: false }); @@ -118,7 +118,7 @@ export async function add(storeName: string, data: T): Promise { return new Promise((resolve, reject) => { const transaction = database.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); - + // 确保数据可以被IndexedDB存储(深拷贝并序列化) const serializedData = JSON.parse(JSON.stringify(data)); const request = store.add(serializedData); @@ -129,7 +129,11 @@ export async function add(storeName: string, data: T): Promise { request.onerror = () => { console.error('IndexedDB add error:', request.error); - reject(new Error(`Failed to add data to ${storeName}: ${request.error?.message}`)); + reject( + new Error( + `Failed to add data to ${storeName}: ${request.error?.message}`, + ), + ); }; }); } @@ -140,7 +144,7 @@ export async function update(storeName: string, data: T): Promise { return new Promise((resolve, reject) => { const transaction = database.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); - + // 确保数据可以被IndexedDB存储(深拷贝并序列化) const serializedData = JSON.parse(JSON.stringify(data)); const request = store.put(serializedData); @@ -151,7 +155,11 @@ export async function update(storeName: string, data: T): Promise { request.onerror = () => { console.error('IndexedDB update error:', request.error); - reject(new Error(`Failed to update data in ${storeName}: ${request.error?.message}`)); + reject( + new Error( + `Failed to update data in ${storeName}: ${request.error?.message}`, + ), + ); }; }); } @@ -175,7 +183,7 @@ export async function remove(storeName: string, id: string): Promise { } // 通用的获取单条数据方法 -export async function get(storeName: string, id: string): Promise { +export async function get(storeName: string, id: string): Promise { const database = await getDB(); return new Promise((resolve, reject) => { const transaction = database.transaction([storeName], 'readonly'); @@ -252,7 +260,10 @@ export async function clear(storeName: string): Promise { } // 批量添加数据 -export async function addBatch(storeName: string, dataList: T[]): Promise { +export async function addBatch( + storeName: string, + dataList: T[], +): Promise { const database = await getDB(); return new Promise((resolve, reject) => { const transaction = database.transaction([storeName], 'readwrite'); @@ -270,17 +281,21 @@ export async function addBatch(storeName: string, dataList: T[]): Promise { console.error('IndexedDB addBatch error:', transaction.error); - reject(new Error(`Failed to add batch data to ${storeName}: ${transaction.error?.message}`)); + reject( + new Error( + `Failed to add batch data to ${storeName}: ${transaction.error?.message}`, + ), + ); }; }); } // 导出数据库 export async function exportDatabase(): Promise<{ - transactions: Transaction[]; categories: Category[]; - persons: Person[]; loans: Loan[]; + persons: Person[]; + transactions: Transaction[]; }> { const transactions = await getAll(STORES.TRANSACTIONS); const categories = await getAll(STORES.CATEGORIES); @@ -297,10 +312,10 @@ export async function exportDatabase(): Promise<{ // 导入数据库 export async function importDatabase(data: { - transactions?: Transaction[]; categories?: Category[]; - persons?: Person[]; loans?: Loan[]; + persons?: Person[]; + transactions?: Transaction[]; }): Promise { if (data.categories) { await clear(STORES.CATEGORIES); @@ -321,4 +336,4 @@ export async function importDatabase(data: { await clear(STORES.LOANS); await addBatch(STORES.LOANS, data.loans); } -} \ No newline at end of file +} diff --git a/apps/web-finance/src/utils/export.ts b/apps/web-finance/src/utils/export.ts index 000344ff..b6ac3ac1 100644 --- a/apps/web-finance/src/utils/export.ts +++ b/apps/web-finance/src/utils/export.ts @@ -1,4 +1,4 @@ -import type { Transaction, Category, Person } from '#/types/finance'; +import type { Category, Person, Transaction } from '#/types/finance'; import dayjs from 'dayjs'; @@ -12,38 +12,44 @@ export function exportToCSV(data: any[], filename: string) { // 获取所有列名 const headers = Object.keys(data[0]); - + // 创建CSV内容 let csvContent = '\uFEFF'; // UTF-8 BOM - + // 添加表头 - csvContent += headers.join(',') + '\n'; - + csvContent += `${headers.join(',')}\n`; + // 添加数据行 - data.forEach(row => { - const values = headers.map(header => { + data.forEach((row) => { + const values = headers.map((header) => { const value = row[header]; // 处理包含逗号或换行符的值 - if (typeof value === 'string' && (value.includes(',') || value.includes('\n'))) { - return `"${value.replace(/"/g, '""')}"`; + if ( + typeof value === 'string' && + (value.includes(',') || value.includes('\n')) + ) { + return `"${value.replaceAll('"', '""')}"`; } return value ?? ''; }); - csvContent += values.join(',') + '\n'; + csvContent += `${values.join(',')}\n`; }); - + // 创建Blob并下载 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); - + link.setAttribute('href', url); - link.setAttribute('download', `${filename}_${dayjs().format('YYYYMMDD_HHmmss')}.csv`); + link.setAttribute( + 'download', + `${filename}_${dayjs().format('YYYYMMDD_HHmmss')}.csv`, + ); link.style.visibility = 'hidden'; - - document.body.appendChild(link); + + document.body.append(link); link.click(); - document.body.removeChild(link); + link.remove(); } /** @@ -52,14 +58,14 @@ export function exportToCSV(data: any[], filename: string) { export function exportTransactions( transactions: Transaction[], categories: Category[], - persons: Person[] + persons: Person[], ) { // 创建分类和人员的映射 - const categoryMap = new Map(categories.map(c => [c.id, c.name])); - const personMap = new Map(persons.map(p => [p.id, p.name])); - + const categoryMap = new Map(categories.map((c) => [c.id, c.name])); + const personMap = new Map(persons.map((p) => [p.id, p.name])); + // 转换交易数据为导出格式 - const exportData = transactions.map(t => ({ + const exportData = transactions.map((t) => ({ 日期: t.date, 类型: t.type === 'income' ? '收入' : '支出', 分类: categoryMap.get(t.categoryId) || '', @@ -70,13 +76,18 @@ export function exportTransactions( 收款人: t.payee || '', 数量: t.quantity, 单价: t.quantity > 1 ? (t.amount / t.quantity).toFixed(2) : t.amount, - 状态: t.status === 'completed' ? '已完成' : t.status === 'pending' ? '待处理' : '已取消', + 状态: + t.status === 'completed' + ? '已完成' + : t.status === 'pending' + ? '待处理' + : '已取消', 描述: t.description || '', 记录人: t.recorder || '', 创建时间: t.created_at, - 更新时间: t.updated_at + 更新时间: t.updated_at, })); - + exportToCSV(exportData, '交易记录'); } @@ -85,18 +96,23 @@ export function exportTransactions( */ export function exportToJSON(data: any, filename: string) { const jsonContent = JSON.stringify(data, null, 2); - - const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' }); + + const blob = new Blob([jsonContent], { + type: 'application/json;charset=utf-8;', + }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); - + link.setAttribute('href', url); - link.setAttribute('download', `${filename}_${dayjs().format('YYYYMMDD_HHmmss')}.json`); + link.setAttribute( + 'download', + `${filename}_${dayjs().format('YYYYMMDD_HHmmss')}.json`, + ); link.style.visibility = 'hidden'; - - document.body.appendChild(link); + + document.body.append(link); link.click(); - document.body.removeChild(link); + link.remove(); } /** @@ -108,7 +124,7 @@ export function generateImportTemplate() { date: '2025-08-05', type: 'expense', category: '餐饮', - amount: 100.00, + amount: 100, currency: 'CNY', description: '午餐', project: '项目名称', @@ -121,7 +137,7 @@ export function generateImportTemplate() { date: '2025-08-05', type: 'income', category: '工资', - amount: 5000.00, + amount: 5000, currency: 'CNY', description: '月薪', project: '', @@ -131,7 +147,7 @@ export function generateImportTemplate() { tags: '', }, ]; - + exportToCSV(template, 'transaction_import_template'); } @@ -141,7 +157,7 @@ export function generateImportTemplate() { export function exportAllData( transactions: Transaction[], categories: Category[], - persons: Person[] + persons: Person[], ) { const exportData = { version: '1.0', @@ -149,10 +165,10 @@ export function exportAllData( data: { transactions, categories, - persons - } + persons, + }, }; - + exportToJSON(exportData, '财务数据备份'); } @@ -160,22 +176,22 @@ export function exportAllData( * 解析CSV文件 */ export function parseCSV(text: string): Record[] { - const lines = text.split('\n').filter(line => line.trim()); + const lines = text.split('\n').filter((line) => line.trim()); if (lines.length === 0) return []; - + // 解析表头 - const headers = lines[0].split(',').map(h => h.trim()); - + const headers = lines[0].split(',').map((h) => h.trim()); + // 解析数据行 const data = []; for (let i = 1; i < lines.length; i++) { const values = []; let current = ''; let inQuotes = false; - + for (let j = 0; j < lines[i].length; j++) { const char = lines[i][j]; - + if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { @@ -186,7 +202,7 @@ export function parseCSV(text: string): Record[] { } } values.push(current.trim()); - + // 创建对象 const row: Record = {}; headers.forEach((header, index) => { @@ -194,6 +210,6 @@ export function parseCSV(text: string): Record[] { }); data.push(row); } - + return data; -} \ No newline at end of file +} diff --git a/apps/web-finance/src/utils/import.ts b/apps/web-finance/src/utils/import.ts index 034737e1..117365e5 100644 --- a/apps/web-finance/src/utils/import.ts +++ b/apps/web-finance/src/utils/import.ts @@ -1,4 +1,4 @@ -import type { Transaction, Category, Person } from '#/types/finance'; +import type { Category, Person, Transaction } from '#/types/finance'; import dayjs from 'dayjs'; import { v4 as uuidv4 } from 'uuid'; @@ -7,16 +7,20 @@ import { v4 as uuidv4 } from 'uuid'; * 解析CSV文本 */ export function parseCSV(text: string): Record[] { - const lines = text.split('\n').filter(line => line.trim()); + const lines = text.split('\n').filter((line) => line.trim()); if (lines.length < 2) return []; - + // 解析表头 - const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, '')); - + const headers = lines[0] + .split(',') + .map((h) => h.trim().replaceAll(/^"|"$/g, '')); + // 解析数据行 const data: Record[] = []; for (let i = 1; i < lines.length; i++) { - const values = lines[i].split(',').map(v => v.trim().replace(/^"|"$/g, '')); + const values = lines[i] + .split(',') + .map((v) => v.trim().replaceAll(/^"|"$/g, '')); if (values.length === headers.length) { const row: Record = {}; headers.forEach((header, index) => { @@ -25,7 +29,7 @@ export function parseCSV(text: string): Record[] { data.push(row); } } - + return data; } @@ -35,26 +39,26 @@ export function parseCSV(text: string): Record[] { export function importTransactionsFromCSV( csvData: Record[], categories: Category[], - persons: Person[] -): { - transactions: Partial[], - errors: string[], - newCategories: string[], - newPersons: string[] + persons: Person[], +): { + errors: string[]; + newCategories: string[]; + newPersons: string[]; + transactions: Partial[]; } { const transactions: Partial[] = []; const errors: string[] = []; const newCategories = new Set(); const newPersons = new Set(); - + // 创建分类和人员的反向映射(名称到ID) - const categoryMap = new Map(categories.map(c => [c.name, c])); - + const categoryMap = new Map(categories.map((c) => [c.name, c])); + csvData.forEach((row, index) => { try { // 解析类型 const type = row['类型'] === '收入' ? 'income' : 'expense'; - + // 查找或标记新分类 let categoryId = ''; const categoryName = row['分类']; @@ -66,34 +70,36 @@ export function importTransactionsFromCSV( newCategories.add(categoryName); } } - + // 标记新的人员 - if (row['付款人'] && !persons.some(p => p.name === row['付款人'])) { + if (row['付款人'] && !persons.some((p) => p.name === row['付款人'])) { newPersons.add(row['付款人']); } - if (row['收款人'] && !persons.some(p => p.name === row['收款人'])) { + if (row['收款人'] && !persons.some((p) => p.name === row['收款人'])) { newPersons.add(row['收款人']); } - + // 解析金额 - const amount = parseFloat(row['金额']); + const amount = Number.parseFloat(row['金额']); if (isNaN(amount)) { errors.push(`第${index + 2}行: 金额格式错误`); return; } - + // 解析日期 - const date = row['日期'] ? dayjs(row['日期']).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD'); + const date = row['日期'] + ? dayjs(row['日期']).format('YYYY-MM-DD') + : dayjs().format('YYYY-MM-DD'); if (!dayjs(date).isValid()) { errors.push(`第${index + 2}行: 日期格式错误`); return; } - + // 解析状态 - let status: 'pending' | 'completed' | 'cancelled' = 'completed'; + let status: 'cancelled' | 'completed' | 'pending' = 'completed'; if (row['状态'] === '待处理') status = 'pending'; else if (row['状态'] === '已取消') status = 'cancelled'; - + // 创建交易对象 const transaction: Partial = { id: uuidv4(), @@ -105,25 +111,25 @@ export function importTransactionsFromCSV( project: row['项目'] || '', payer: row['付款人'] || '', payee: row['收款人'] || '', - quantity: parseInt(row['数量']) || 1, + quantity: Number.parseInt(row['数量']) || 1, status, description: row['描述'] || '', recorder: row['记录人'] || '导入', created_at: dayjs().format('YYYY-MM-DD HH:mm:ss'), - updated_at: dayjs().format('YYYY-MM-DD HH:mm:ss') + updated_at: dayjs().format('YYYY-MM-DD HH:mm:ss'), }; - + transactions.push(transaction); - } catch (error) { + } catch { errors.push(`第${index + 2}行: 数据解析错误`); } }); - + return { transactions, errors, - newCategories: Array.from(newCategories), - newPersons: Array.from(newPersons) + newCategories: [...newCategories], + newPersons: [...newPersons], }; } @@ -131,65 +137,69 @@ export function importTransactionsFromCSV( * 导入JSON备份数据 */ export function importFromJSON(jsonData: any): { - valid: boolean, data?: { - transactions: Transaction[], - categories: Category[], - persons: Person[] - }, - error?: string + categories: Category[]; + persons: Person[]; + transactions: Transaction[]; + }; + error?: string; + valid: boolean; } { try { // 验证数据格式 if (!jsonData.version || !jsonData.data) { return { valid: false, error: '无效的备份文件格式' }; } - + const { transactions, categories, persons } = jsonData.data; - + // 验证必要字段 - if (!Array.isArray(transactions) || !Array.isArray(categories) || !Array.isArray(persons)) { + if ( + !Array.isArray(transactions) || + !Array.isArray(categories) || + !Array.isArray(persons) + ) { return { valid: false, error: '备份数据不完整' }; } - + // 为导入的数据生成新的ID(避免冲突) const idMap = new Map(); - + // 处理分类 - const newCategories = categories.map(c => { + const newCategories = categories.map((c) => { const newId = uuidv4(); idMap.set(c.id, newId); return { ...c, id: newId }; }); - + // 处理人员 - const newPersons = persons.map(p => { + const newPersons = persons.map((p) => { const newId = uuidv4(); idMap.set(p.id, newId); return { ...p, id: newId }; }); - + // 处理交易(更新关联的ID) - const newTransactions = transactions.map(t => { + const newTransactions = transactions.map((t) => { const newId = uuidv4(); return { ...t, id: newId, categoryId: idMap.get(t.categoryId) || t.categoryId, created_at: t.created_at || dayjs().format('YYYY-MM-DD HH:mm:ss'), - updated_at: dayjs().format('YYYY-MM-DD HH:mm:ss') + updated_at: dayjs().format('YYYY-MM-DD HH:mm:ss'), }; }); - + return { valid: true, data: { transactions: newTransactions, categories: newCategories, - persons: newPersons - } + persons: newPersons, + }, }; - } catch (error) { + } catch { return { valid: false, error: '解析备份文件失败' }; } } @@ -200,7 +210,7 @@ export function importFromJSON(jsonData: any): { export function readFileAsText(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onload = (e) => resolve(e.target?.result as string); + reader.addEventListener('load', (e) => resolve(e.target?.result as string)); reader.onerror = reject; reader.readAsText(file); }); @@ -222,9 +232,9 @@ export function generateImportTemplate(): string { '数量', '状态', '描述', - '记录人' + '记录人', ]; - + const examples = [ [ dayjs().format('YYYY-MM-DD'), @@ -238,7 +248,7 @@ export function generateImportTemplate(): string { '1', '已完成', '午餐', - '管理员' + '管理员', ], [ dayjs().subtract(1, 'day').format('YYYY-MM-DD'), @@ -252,15 +262,15 @@ export function generateImportTemplate(): string { '1', '已完成', '月薪', - '管理员' - ] + '管理员', + ], ]; - + let csvContent = '\uFEFF'; // UTF-8 BOM - csvContent += headers.join(',') + '\n'; - examples.forEach(row => { - csvContent += row.join(',') + '\n'; + csvContent += `${headers.join(',')}\n`; + examples.forEach((row) => { + csvContent += `${row.join(',')}\n`; }); - + return csvContent; -} \ No newline at end of file +} diff --git a/apps/web-finance/src/views/analytics/components/BudgetComparison.vue b/apps/web-finance/src/views/analytics/components/BudgetComparison.vue new file mode 100644 index 00000000..cd42d1df --- /dev/null +++ b/apps/web-finance/src/views/analytics/components/BudgetComparison.vue @@ -0,0 +1,394 @@ + + + + + \ No newline at end of file diff --git a/apps/web-finance/src/views/analytics/components/CategoryPieChart.vue b/apps/web-finance/src/views/analytics/components/CategoryPieChart.vue index add21536..53dedae9 100644 --- a/apps/web-finance/src/views/analytics/components/CategoryPieChart.vue +++ b/apps/web-finance/src/views/analytics/components/CategoryPieChart.vue @@ -1,9 +1,3 @@ - - + + \ No newline at end of file + diff --git a/apps/web-finance/src/views/analytics/components/KeyMetricsCards.vue b/apps/web-finance/src/views/analytics/components/KeyMetricsCards.vue new file mode 100644 index 00000000..ce1c011b --- /dev/null +++ b/apps/web-finance/src/views/analytics/components/KeyMetricsCards.vue @@ -0,0 +1,248 @@ + + + + + \ No newline at end of file diff --git a/apps/web-finance/src/views/analytics/components/MonthlyComparisonChart.vue b/apps/web-finance/src/views/analytics/components/MonthlyComparisonChart.vue index c6726fec..727d76a5 100644 --- a/apps/web-finance/src/views/analytics/components/MonthlyComparisonChart.vue +++ b/apps/web-finance/src/views/analytics/components/MonthlyComparisonChart.vue @@ -1,18 +1,13 @@ - - + + \ No newline at end of file + diff --git a/apps/web-finance/src/views/analytics/components/PersonAnalysisChart.vue b/apps/web-finance/src/views/analytics/components/PersonAnalysisChart.vue index 55cb8463..52acd73a 100644 --- a/apps/web-finance/src/views/analytics/components/PersonAnalysisChart.vue +++ b/apps/web-finance/src/views/analytics/components/PersonAnalysisChart.vue @@ -1,9 +1,3 @@ - - + + \ No newline at end of file + diff --git a/apps/web-finance/src/views/analytics/components/SmartInsights.vue b/apps/web-finance/src/views/analytics/components/SmartInsights.vue new file mode 100644 index 00000000..960eed81 --- /dev/null +++ b/apps/web-finance/src/views/analytics/components/SmartInsights.vue @@ -0,0 +1,598 @@ + + + + + \ No newline at end of file diff --git a/apps/web-finance/src/views/analytics/components/TagCloudAnalysis.vue b/apps/web-finance/src/views/analytics/components/TagCloudAnalysis.vue new file mode 100644 index 00000000..e7bf1453 --- /dev/null +++ b/apps/web-finance/src/views/analytics/components/TagCloudAnalysis.vue @@ -0,0 +1,447 @@ + + + + + \ No newline at end of file diff --git a/apps/web-finance/src/views/analytics/components/TimeDimensionAnalysis.vue b/apps/web-finance/src/views/analytics/components/TimeDimensionAnalysis.vue new file mode 100644 index 00000000..f4a89aa7 --- /dev/null +++ b/apps/web-finance/src/views/analytics/components/TimeDimensionAnalysis.vue @@ -0,0 +1,490 @@ + + + + + \ No newline at end of file diff --git a/apps/web-finance/src/views/analytics/components/TrendChart.vue b/apps/web-finance/src/views/analytics/components/TrendChart.vue index deb3c2ad..1bc1f537 100644 --- a/apps/web-finance/src/views/analytics/components/TrendChart.vue +++ b/apps/web-finance/src/views/analytics/components/TrendChart.vue @@ -1,22 +1,17 @@ - - + + \ No newline at end of file + diff --git a/apps/web-finance/src/views/analytics/overview/index.vue b/apps/web-finance/src/views/analytics/overview/index.vue index 4c2581c8..fa039bb1 100644 --- a/apps/web-finance/src/views/analytics/overview/index.vue +++ b/apps/web-finance/src/views/analytics/overview/index.vue @@ -1,112 +1,39 @@ - - \ No newline at end of file + + + diff --git a/apps/web-finance/src/views/analytics/reports/custom.vue b/apps/web-finance/src/views/analytics/reports/custom.vue index 22fa1c82..976c6d8c 100644 --- a/apps/web-finance/src/views/analytics/reports/custom.vue +++ b/apps/web-finance/src/views/analytics/reports/custom.vue @@ -5,9 +5,7 @@ import { Card } from 'ant-design-vue'; \ No newline at end of file + diff --git a/apps/web-finance/src/views/analytics/reports/daily.vue b/apps/web-finance/src/views/analytics/reports/daily.vue index 2e64b1ef..a12fc703 100644 --- a/apps/web-finance/src/views/analytics/reports/daily.vue +++ b/apps/web-finance/src/views/analytics/reports/daily.vue @@ -5,9 +5,7 @@ import { Card } from 'ant-design-vue'; \ No newline at end of file + diff --git a/apps/web-finance/src/views/analytics/reports/monthly.vue b/apps/web-finance/src/views/analytics/reports/monthly.vue index bae9da90..da138b13 100644 --- a/apps/web-finance/src/views/analytics/reports/monthly.vue +++ b/apps/web-finance/src/views/analytics/reports/monthly.vue @@ -5,9 +5,7 @@ import { Card } from 'ant-design-vue'; \ No newline at end of file + diff --git a/apps/web-finance/src/views/analytics/reports/yearly.vue b/apps/web-finance/src/views/analytics/reports/yearly.vue index d1a9c41e..09cdefa7 100644 --- a/apps/web-finance/src/views/analytics/reports/yearly.vue +++ b/apps/web-finance/src/views/analytics/reports/yearly.vue @@ -5,9 +5,7 @@ import { Card } from 'ant-design-vue'; \ No newline at end of file + diff --git a/apps/web-finance/src/views/analytics/trends/index.vue b/apps/web-finance/src/views/analytics/trends/index.vue index 77db35f4..5db70d25 100644 --- a/apps/web-finance/src/views/analytics/trends/index.vue +++ b/apps/web-finance/src/views/analytics/trends/index.vue @@ -5,9 +5,7 @@ import { Card } from 'ant-design-vue'; \ No newline at end of file + diff --git a/apps/web-finance/src/views/dashboard/workspace/index.vue b/apps/web-finance/src/views/dashboard/workspace/index.vue index 4a63d37a..b3d0c895 100644 --- a/apps/web-finance/src/views/dashboard/workspace/index.vue +++ b/apps/web-finance/src/views/dashboard/workspace/index.vue @@ -244,7 +244,11 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
- +
diff --git a/apps/web-finance/src/views/finance/budget/components/budget-setting.vue b/apps/web-finance/src/views/finance/budget/components/budget-setting.vue index 0073e8ee..867546ee 100644 --- a/apps/web-finance/src/views/finance/budget/components/budget-setting.vue +++ b/apps/web-finance/src/views/finance/budget/components/budget-setting.vue @@ -1,119 +1,22 @@ - - \ No newline at end of file + + + diff --git a/apps/web-finance/src/views/finance/budget/index.vue b/apps/web-finance/src/views/finance/budget/index.vue index f6cd3bdd..4b42b1bd 100644 --- a/apps/web-finance/src/views/finance/budget/index.vue +++ b/apps/web-finance/src/views/finance/budget/index.vue @@ -1,190 +1,7 @@ - - + + \ No newline at end of file + diff --git a/apps/web-finance/src/views/finance/category-stats/index.vue b/apps/web-finance/src/views/finance/category-stats/index.vue new file mode 100644 index 00000000..82c5ccdd --- /dev/null +++ b/apps/web-finance/src/views/finance/category-stats/index.vue @@ -0,0 +1,643 @@ + + + + + \ No newline at end of file diff --git a/apps/web-finance/src/views/finance/category/components/category-form.vue b/apps/web-finance/src/views/finance/category/components/category-form.vue index b85df270..75bac634 100644 --- a/apps/web-finance/src/views/finance/category/components/category-form.vue +++ b/apps/web-finance/src/views/finance/category/components/category-form.vue @@ -1,20 +1,12 @@ + + \ No newline at end of file + diff --git a/apps/web-finance/src/views/finance/mobile/index.vue b/apps/web-finance/src/views/finance/mobile/index.vue index 7cb41948..a41fcc78 100644 --- a/apps/web-finance/src/views/finance/mobile/index.vue +++ b/apps/web-finance/src/views/finance/mobile/index.vue @@ -1,18 +1,31 @@ + + - - \ No newline at end of file + diff --git a/apps/web-finance/src/views/finance/mobile/more.vue b/apps/web-finance/src/views/finance/mobile/more.vue index 8ae2d5bd..ad17367b 100644 --- a/apps/web-finance/src/views/finance/mobile/more.vue +++ b/apps/web-finance/src/views/finance/mobile/more.vue @@ -1,174 +1,9 @@ - - + + \ No newline at end of file + diff --git a/apps/web-finance/src/views/finance/mobile/quick-add.vue b/apps/web-finance/src/views/finance/mobile/quick-add.vue index 89261ab0..1f5b2144 100644 --- a/apps/web-finance/src/views/finance/mobile/quick-add.vue +++ b/apps/web-finance/src/views/finance/mobile/quick-add.vue @@ -1,172 +1,8 @@ -