From 675fe0a1a8b616ecc5f4ade0d0969d692f407007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E7=9A=84=E7=94=A8=E6=88=B7=E5=90=8D?= <你的邮箱> Date: Sun, 24 Aug 2025 16:41:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=B4=A2=E5=8A=A1?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F=E5=8A=9F=E8=83=BD=E4=B8=8E?= =?UTF-8?q?=E5=88=86=E6=9E=90=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要更新: - 🎯 新增综合分析仪表板,包含关键指标卡片、预算对比、智能洞察等组件 - 📊 增强数据可视化能力,新增标签云分析、时间维度分析等图表 - 📱 优化移动端响应式设计,改进触控交互体验 - 🔧 新增多个API模块(base、budget、tag),完善数据管理 - 🗂️ 重构路由结构,新增贷款、快速添加、设置、统计等独立模块 - 🔄 优化数据导入导出功能,增强数据迁移能力 - 🐛 修复多个已知问题,提升系统稳定性 技术改进: - 使用IndexedDB提升本地存储性能 - 实现模拟API服务,支持离线开发 - 增加自动化测试脚本,确保功能稳定 - 优化打包配置,提升构建效率 文件变更: - 新增42个文件 - 修改55个文件 - 包含测试脚本、配置文件、组件和API模块 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- analytics-complete-success.png | Bin 0 -> 33325 bytes analytics-debug.png | Bin 0 -> 33325 bytes analytics-overview.png | Bin 0 -> 33325 bytes analytics-success.png | Bin 0 -> 33325 bytes apps/backend-mock/utils/mock-data.ts | 171 ++++- apps/web-finance/README.md | 13 +- apps/web-finance/check-server.js | 35 +- apps/web-finance/manual-check.js | 26 +- apps/web-finance/quick-test.js | 21 +- apps/web-finance/src/api/finance/base.ts | 18 + apps/web-finance/src/api/finance/budget.ts | 58 ++ apps/web-finance/src/api/finance/category.ts | 4 +- apps/web-finance/src/api/finance/index.ts | 10 +- apps/web-finance/src/api/finance/loan.ts | 14 +- apps/web-finance/src/api/finance/person.ts | 4 +- apps/web-finance/src/api/finance/tag.ts | 81 +++ .../src/api/finance/transaction.ts | 18 +- apps/web-finance/src/api/mock/finance-data.ts | 67 +- .../src/api/mock/finance-service.ts | 347 ++++++---- apps/web-finance/src/api/mock/index.ts | 5 + apps/web-finance/src/bootstrap.ts | 5 +- .../src/components/charts/useChart.ts | 31 +- .../src/locales/langs/zh-CN/analytics.json | 10 +- .../src/locales/langs/zh-CN/finance.json | 25 +- .../src/locales/langs/zh-CN/tools.json | 12 +- .../src/router/routes/modules/analytics.ts | 2 +- .../src/router/routes/modules/finance.ts | 103 --- .../src/router/routes/modules/loan.ts | 29 + .../src/router/routes/modules/quick-add.ts | 30 + .../src/router/routes/modules/settings.ts | 56 ++ .../src/router/routes/modules/statistics.ts | 56 ++ .../src/router/routes/modules/tools.ts | 41 +- .../src/router/routes/modules/transactions.ts | 30 + apps/web-finance/src/store/modules/budget.ts | 85 ++- .../web-finance/src/store/modules/category.ts | 6 +- apps/web-finance/src/store/modules/loan.ts | 17 +- apps/web-finance/src/store/modules/person.ts | 2 +- apps/web-finance/src/store/modules/tag.ts | 26 +- .../src/store/modules/transaction.ts | 20 +- apps/web-finance/src/styles/mobile.css | 46 +- apps/web-finance/src/types/finance.ts | 20 +- apps/web-finance/src/utils/data-migration.ts | 41 +- apps/web-finance/src/utils/db.ts | 59 +- apps/web-finance/src/utils/export.ts | 110 +-- apps/web-finance/src/utils/import.ts | 144 ++-- .../analytics/components/BudgetComparison.vue | 394 +++++++++++ .../analytics/components/CategoryPieChart.vue | 32 +- .../analytics/components/KeyMetricsCards.vue | 248 +++++++ .../components/MonthlyComparisonChart.vue | 51 +- .../components/PersonAnalysisChart.vue | 50 +- .../analytics/components/SmartInsights.vue | 598 ++++++++++++++++ .../analytics/components/TagCloudAnalysis.vue | 447 ++++++++++++ .../components/TimeDimensionAnalysis.vue | 490 +++++++++++++ .../views/analytics/components/TrendChart.vue | 52 +- .../src/views/analytics/overview/index.vue | 282 +++++--- .../src/views/analytics/reports/custom.vue | 6 +- .../src/views/analytics/reports/daily.vue | 6 +- .../src/views/analytics/reports/monthly.vue | 6 +- .../src/views/analytics/reports/yearly.vue | 6 +- .../src/views/analytics/trends/index.vue | 6 +- .../src/views/dashboard/workspace/index.vue | 6 +- .../budget/components/budget-setting.vue | 273 ++++---- .../src/views/finance/budget/index.vue | 402 +++++------ .../views/finance/category-stats/index.vue | 643 ++++++++++++++++++ .../category/components/category-form.vue | 84 +-- .../src/views/finance/category/index.vue | 83 ++- .../src/views/finance/dashboard/index.vue | 12 +- .../finance/loan/components/loan-form.vue | 114 ++-- .../loan/components/repayment-form.vue | 78 ++- .../src/views/finance/loan/index.vue | 180 ++--- .../src/views/finance/mobile/budget.vue | 420 ++++++------ .../src/views/finance/mobile/index.vue | 43 +- .../src/views/finance/mobile/more.vue | 404 +++++------ .../src/views/finance/mobile/quick-add.vue | 467 ++++++------- .../src/views/finance/mobile/statistics.vue | 325 +++++---- .../views/finance/mobile/transaction-list.vue | 613 +++++++++-------- .../finance/person/components/person-form.vue | 97 ++- .../src/views/finance/person/index.vue | 54 +- .../src/views/finance/quick-add/index.vue | 63 ++ .../src/views/finance/responsive-wrapper.vue | 24 +- .../finance/tag/components/tag-selector.vue | 176 ++--- .../src/views/finance/tag/index.vue | 392 +++++------ .../src/views/finance/test-api.vue | 75 +- .../transaction/components/import-export.vue | 112 +-- .../components/transaction-form.vue | 491 +++++++------ .../src/views/finance/transaction/index.vue | 140 ++-- .../src/views/tools/backup/index.vue | 6 +- .../src/views/tools/budget/index.vue | 6 +- .../src/views/tools/export/index.vue | 6 +- .../src/views/tools/import/index.vue | 6 +- .../src/views/tools/tags/index.vue | 6 +- apps/web-finance/test-all-menus.js | 77 ++- apps/web-finance/test-analytics-charts.js | 74 +- apps/web-finance/test-analytics-simple.js | 44 +- apps/web-finance/test-finance-system.js | 83 ++- apps/web-finance/test-import-export.js | 61 +- apps/web-finance/test-menu-navigation.js | 87 +-- apps/web-finance/test-menu-switch.js | 39 +- apps/web-finance/test-menus-with-auth.js | 113 +-- apps/web-finance/test-results/.last-run.json | 4 + apps/web-finance/test-summary.md | 5 +- apps/web-finance/test-system.js | 70 +- apps/web-finance/test-transaction-form.js | 51 +- apps/web-finance/vite.config.mts | 2 +- bar-chart-view.png | Bin 0 -> 80714 bytes cache-clear-error.png | Bin 0 -> 149429 bytes category-stats-error.png | Bin 0 -> 10356 bytes category-stats-page.png | Bin 0 -> 103455 bytes check-console.js | 33 +- console-check.png | Bin 0 -> 10408 bytes current-state.png | Bin 0 -> 95633 bytes error-state.png | Bin 0 -> 167463 bytes expanded-menu.png | Bin 0 -> 232114 bytes final-menu-structure.png | Bin 0 -> 237044 bytes menu-structure-check.png | Bin 0 -> 220201 bytes new-menu-structure.png | Bin 0 -> 251435 bytes reload-error.png | Bin 0 -> 126451 bytes simple-menu-test.png | Bin 0 -> 225911 bytes simple-test.js | 15 +- test-analytics-complete.js | 187 +++++ test-analytics-debug.js | 144 ++++ test-analytics-features.js | 139 ++++ test-analytics-final.js | 144 ++++ test-auto-login.js | 78 +-- test-category-stats-fixed.js | 87 +++ test-category-stats.js | 104 +++ test-check-menu-full.js | 109 +++ test-check.js | 59 +- test-clear-cache-menu.js | 111 +++ test-complete.js | 115 ++-- test-console-errors.js | 46 ++ test-create-transaction.js | 116 ++-- test-direct.js | 97 +-- test-error-final.png | Bin 0 -> 99240 bytes test-error.png | Bin 120111 -> 106945 bytes test-expand-menu.js | 103 +++ test-final-menu.js | 43 ++ test-final-success.js | 50 +- test-horizontal-layout.html | 221 ++++++ test-menu-reload.js | 59 ++ test-new-menu.js | 88 +++ test-new-ui.js | 110 +++ test-quick-verify.js | 64 ++ test-results/.last-run.json | 4 + test-simple-menu.js | 56 ++ test-simple.js | 33 +- test-stats-final.js | 86 +++ test-stats-simple.js | 53 ++ test-transaction-final.js | 68 +- test-transaction-order.js | 127 ++++ test-transaction-page.js | 42 +- test-transaction.js | 49 +- test-with-slider.js | 90 +-- trend-chart-view.png | Bin 0 -> 109570 bytes 154 files changed, 10035 insertions(+), 3978 deletions(-) create mode 100644 analytics-complete-success.png create mode 100644 analytics-debug.png create mode 100644 analytics-overview.png create mode 100644 analytics-success.png create mode 100644 apps/web-finance/src/api/finance/base.ts create mode 100644 apps/web-finance/src/api/finance/budget.ts create mode 100644 apps/web-finance/src/api/finance/tag.ts create mode 100644 apps/web-finance/src/api/mock/index.ts delete mode 100644 apps/web-finance/src/router/routes/modules/finance.ts create mode 100644 apps/web-finance/src/router/routes/modules/loan.ts create mode 100644 apps/web-finance/src/router/routes/modules/quick-add.ts create mode 100644 apps/web-finance/src/router/routes/modules/settings.ts create mode 100644 apps/web-finance/src/router/routes/modules/statistics.ts create mode 100644 apps/web-finance/src/router/routes/modules/transactions.ts create mode 100644 apps/web-finance/src/views/analytics/components/BudgetComparison.vue create mode 100644 apps/web-finance/src/views/analytics/components/KeyMetricsCards.vue create mode 100644 apps/web-finance/src/views/analytics/components/SmartInsights.vue create mode 100644 apps/web-finance/src/views/analytics/components/TagCloudAnalysis.vue create mode 100644 apps/web-finance/src/views/analytics/components/TimeDimensionAnalysis.vue create mode 100644 apps/web-finance/src/views/finance/category-stats/index.vue create mode 100644 apps/web-finance/src/views/finance/quick-add/index.vue create mode 100644 apps/web-finance/test-results/.last-run.json create mode 100644 bar-chart-view.png create mode 100644 cache-clear-error.png create mode 100644 category-stats-error.png create mode 100644 category-stats-page.png create mode 100644 console-check.png create mode 100644 current-state.png create mode 100644 error-state.png create mode 100644 expanded-menu.png create mode 100644 final-menu-structure.png create mode 100644 menu-structure-check.png create mode 100644 new-menu-structure.png create mode 100644 reload-error.png create mode 100644 simple-menu-test.png create mode 100644 test-analytics-complete.js create mode 100644 test-analytics-debug.js create mode 100644 test-analytics-features.js create mode 100644 test-analytics-final.js create mode 100644 test-category-stats-fixed.js create mode 100644 test-category-stats.js create mode 100644 test-check-menu-full.js create mode 100644 test-clear-cache-menu.js create mode 100644 test-console-errors.js create mode 100644 test-error-final.png create mode 100644 test-expand-menu.js create mode 100644 test-final-menu.js create mode 100644 test-horizontal-layout.html create mode 100644 test-menu-reload.js create mode 100644 test-new-menu.js create mode 100644 test-new-ui.js create mode 100644 test-quick-verify.js create mode 100644 test-results/.last-run.json create mode 100644 test-simple-menu.js create mode 100644 test-stats-final.js create mode 100644 test-stats-simple.js create mode 100644 test-transaction-order.js create mode 100644 trend-chart-view.png diff --git a/analytics-complete-success.png b/analytics-complete-success.png new file mode 100644 index 0000000000000000000000000000000000000000..a56a68d1ad15aa1a615b4b0cee8d4961ce40854b GIT binary patch literal 33325 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 I{^-^J0y?8Bj{pDw literal 0 HcmV?d00001 diff --git a/analytics-debug.png b/analytics-debug.png new file mode 100644 index 0000000000000000000000000000000000000000..a56a68d1ad15aa1a615b4b0cee8d4961ce40854b GIT binary patch literal 33325 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 I{^-^J0y?8Bj{pDw literal 0 HcmV?d00001 diff --git a/analytics-overview.png b/analytics-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..a56a68d1ad15aa1a615b4b0cee8d4961ce40854b GIT binary patch literal 33325 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 I{^-^J0y?8Bj{pDw literal 0 HcmV?d00001 diff --git a/analytics-success.png b/analytics-success.png new file mode 100644 index 0000000000000000000000000000000000000000..a56a68d1ad15aa1a615b4b0cee8d4961ce40854b GIT binary patch literal 33325 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 I{^-^J0y?8Bj{pDw literal 0 HcmV?d00001 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 @@ -