From 2976b85967a56f7eeab156e0bc40f2d402e5a2c6 Mon Sep 17 00:00:00 2001 From: rosercode Date: Sun, 20 Sep 2026 17:13:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=A4=E6=98=93=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D=E3=80=81=E8=B4=A6=E5=8D=95=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=E7=BB=9F=E8=AE=A1=E9=BB=98=E8=AE=A4=E5=80=BC=E3=80=81?= =?UTF-8?q?AI=20=E8=A7=84=E5=88=99=E7=94=9F=E6=88=90=E4=B8=8E=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=90=9C=E7=B4=A2=EF=BC=8CAI=20=E6=94=B9=E7=94=A8=20o?= =?UTF-8?q?penai-java=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 交易记录「支付方式」全链路重命名为「交易渠道」(channel):DB 列、实体/DTO/VO、接口 /api/transaction/channels、统计维度 CHANNEL、AI 提示词与 JSON 字段、前端与中英日文案;新增 SchemaMigrator 在启动/切换数据源时幂等迁移列名(MySQL 用 information_schema、SQLite 用 PRAGMA) - 按账单来源设置「计入统计」默认值:AppConfig 持久化 + GET/PUT /api/app/bill-source-include-in-stats,导入解析按「解析器判定 && 来源默认」应用并覆盖上传与邮箱两条路径,设置页新增面板 - AI 生成规则:基于所选已打标签记录归纳分类/标签规则(POST /api/ai/rule/suggest、/api/ai/rule/apply),规则页两步弹窗(选择记录→预览创建),支持候选记录分页/搜索/筛选;按「类型+关键词+分类+标签」签名与已有规则去重,重复项标记「已存在」并禁止导入 - 规则列表新增分页与搜索:GET /api/rule/page(名称/关键词、启用状态、收支类型) - 统计面板 AI 搜索:POST /api/ai/statistics 将自然语言解析为时间/类型/分类/标签/渠道/金额/统计口径/趋势粒度并回填;交互对齐账单列表 AI 查询(弹窗 + 历史 + 示例 + 空条件守卫);统计后端补充标签筛选,仪表盘新增标签筛选与 AI 搜索 - AI 客户端改用 OpenAI 官方 Java SDK(com.openai:openai-java),自定义 baseUrl 指向 DeepSeek,保留 chat 签名与 DeepSeekResponse - AI 数据外发收敛:分类/标签建议与规则生成不再发送 rawData 与金额,仅发送类型/备注/渠道/分类/标签;新增 AiCallType.RULE / STATISTICS - 收支记录页新增「刷新」按钮 --- build.gradle | 3 + frontend/src/api/ai.ts | 79 +- frontend/src/api/app.ts | 12 + frontend/src/api/rule.ts | 14 + frontend/src/api/statistics.ts | 5 +- frontend/src/api/transaction.ts | 17 +- .../src/components/BillSourceStatsPanel.vue | 90 ++ frontend/src/locales/messages.ts | 62 +- frontend/src/utils/{payment.ts => channel.ts} | 8 +- frontend/src/views/Dashboard.vue | 330 +++++-- frontend/src/views/RuleManage.vue | 561 +++++++++++- frontend/src/views/Settings.vue | 4 +- frontend/src/views/TransactionList.vue | 96 ++- .../finance/client/DeepSeekClient.java | 180 ++-- .../finance/config/app/AppConfig.java | 25 + .../config/datasource/SchemaMigrator.java | 132 +++ .../finance/controller/AiController.java | 52 +- .../controller/AppConfigController.java | 56 ++ .../finance/controller/RuleController.java | 15 + .../controller/StatisticsController.java | 2 +- .../controller/TransactionController.java | 12 +- .../finance/dto/AiRuleApplyRequest.java | 62 ++ .../finance/dto/AiRuleSuggestRequest.java | 19 + .../finance/dto/AiSuggestOneRequest.java | 4 +- .../finance/dto/BillSourceStatsRequest.java | 17 + .../cn/rosercode/finance/dto/RuleQuery.java | 27 + .../finance/dto/StatisticsRangeQuery.java | 10 +- .../finance/dto/TransactionQuery.java | 7 +- .../finance/dto/TransactionRequest.java | 4 +- .../rosercode/finance/entity/Transaction.java | 4 +- .../rosercode/finance/enums/AiCallType.java | 8 +- .../finance/enums/StatisticsDimension.java | 6 +- .../finance/service/AiRuleService.java | 31 + .../finance/service/AiStatisticsService.java | 20 + .../finance/service/RuleService.java | 11 + .../finance/service/TransactionService.java | 8 +- .../service/impl/AiQueryServiceImpl.java | 10 +- .../service/impl/AiRecordServiceImpl.java | 18 +- .../service/impl/AiRuleServiceImpl.java | 811 ++++++++++++++++++ .../service/impl/AiStatisticsServiceImpl.java | 469 ++++++++++ .../service/impl/AiSuggestionServiceImpl.java | 34 +- .../service/impl/BillImportServiceImpl.java | 40 +- .../service/impl/DatabaseServiceImpl.java | 8 +- .../finance/service/impl/RuleServiceImpl.java | 43 +- .../service/impl/StatisticsServiceImpl.java | 37 +- .../service/impl/TransactionServiceImpl.java | 64 +- .../rosercode/finance/vo/AiQueryResultVO.java | 4 +- .../finance/vo/AiRecordResultVO.java | 4 +- .../finance/vo/AiRuleSuggestResultVO.java | 20 + .../finance/vo/AiRuleSuggestionVO.java | 57 ++ .../finance/vo/AiStatisticsResultVO.java | 60 ++ .../rosercode/finance/vo/AiSuggestionVO.java | 2 +- .../finance/vo/StatisticsGroupVO.java | 2 +- .../finance/vo/TransactionExportVO.java | 4 +- .../rosercode/finance/vo/TransactionVO.java | 4 +- .../resources/mapper/TransactionMapper.xml | 4 +- src/main/resources/sql/init.sql | 2 +- src/main/resources/sql/schema-sqlite.sql | 2 +- 58 files changed, 3347 insertions(+), 345 deletions(-) create mode 100644 frontend/src/api/app.ts create mode 100644 frontend/src/components/BillSourceStatsPanel.vue rename frontend/src/utils/{payment.ts => channel.ts} (63%) create mode 100644 src/main/java/cn/rosercode/finance/config/datasource/SchemaMigrator.java create mode 100644 src/main/java/cn/rosercode/finance/dto/AiRuleApplyRequest.java create mode 100644 src/main/java/cn/rosercode/finance/dto/AiRuleSuggestRequest.java create mode 100644 src/main/java/cn/rosercode/finance/dto/BillSourceStatsRequest.java create mode 100644 src/main/java/cn/rosercode/finance/dto/RuleQuery.java create mode 100644 src/main/java/cn/rosercode/finance/service/AiRuleService.java create mode 100644 src/main/java/cn/rosercode/finance/service/AiStatisticsService.java create mode 100644 src/main/java/cn/rosercode/finance/service/impl/AiRuleServiceImpl.java create mode 100644 src/main/java/cn/rosercode/finance/service/impl/AiStatisticsServiceImpl.java create mode 100644 src/main/java/cn/rosercode/finance/vo/AiRuleSuggestResultVO.java create mode 100644 src/main/java/cn/rosercode/finance/vo/AiRuleSuggestionVO.java create mode 100644 src/main/java/cn/rosercode/finance/vo/AiStatisticsResultVO.java diff --git a/build.gradle b/build.gradle index ba950b7..80f9c00 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,9 @@ dependencies { // PDF 账单解析(招商银行「交易流水」PDF 为文本型,按字符坐标还原表格) implementation 'org.apache.pdfbox:pdfbox:3.0.3' + // OpenAI Java SDK:调用 DeepSeek 等 OpenAI 兼容接口(通过自定义 baseUrl 兼容) + implementation 'com.openai:openai-java:4.65.0' + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' diff --git a/frontend/src/api/ai.ts b/frontend/src/api/ai.ts index c29a6ab..9b2fee8 100644 --- a/frontend/src/api/ai.ts +++ b/frontend/src/api/ai.ts @@ -16,7 +16,7 @@ export interface AiNewTag { color: string } -/** 单条 AI 建议(按类型、备注、支付方式聚合后的一组记录) */ +/** 单条 AI 建议(按类型、备注、交易渠道聚合后的一组记录) */ export interface AiSuggestion { transactionId: number transactionIds: number[] @@ -78,7 +78,7 @@ export function parseTagByAi(text: string) { } /** 根据表单当前内容生成单条分类与标签建议(调用大模型,超时放宽) */ -export function suggestOneByAi(data: { type: string; amount?: number | null; paymentMethod?: string; remark?: string }) { +export function suggestOneByAi(data: { type: string; amount?: number | null; channel?: string; remark?: string }) { return request.post('/ai/suggest-one', data, { timeout: 60000 }) } @@ -92,6 +92,77 @@ export function suggestColor(name: string) { return request.post('/ai/color', { name }, { timeout: 60000 }) } +/** AI 生成的规则建议 */ +export interface AiRuleSuggestion { + name: string + type: string | null + keyword: string | null + minAmount: number | null + maxAmount: number | null + categoryId: number | null + categoryName: string | null + newCategoryName: string | null + newCategoryIcon: string | null + tagIds: number[] + tagNames: string[] + newTags: AiNewTag[] + reason: string + /** 是否与已有规则重复:重复时默认不勾选且禁止导入 */ + duplicate: boolean +} + +/** AI 生成规则的结果 */ +export interface AiRuleSuggestResult { + summary: string + rules: AiRuleSuggestion[] +} + +/** AI 统计查询解析结果 */ +export interface AiStatisticsResult { + summary: string + types: string[] + categoryIds: number[] + categoryNames: string[] + tagIds: number[] + tagNames: string[] + channels: string[] + startTime: string | null + endTime: string | null + minAmount: number | null + maxAmount: number | null + remark: string | null + statsScope: 'INCLUDED' | 'ALL' | 'EXCLUDED' | null + dimension: 'day' | 'week' | 'month' | 'quarter' | 'year' | null + unmatched: string[] +} + +/** 将自然语言统计查询解析为统计面板参数(调用大模型,超时放宽) */ +export function parseStatisticsByAi(query: string) { + return request.post('/ai/statistics', { query }, { timeout: 120000 }) +} + +/** 根据选中的已打标签记录生成规则建议(调用大模型,超时放宽) */ +export function suggestRulesByAi(transactionIds: number[]) { + return request.post('/ai/rule/suggest', { transactionIds }, { timeout: 120000 }) +} + +/** 批量创建 AI 生成的规则,必要时新建分类与标签 */ +export function applyAiRules(rules: { + name: string + enabled?: boolean + type?: string | null + keyword?: string | null + minAmount?: number | null + maxAmount?: number | null + categoryId?: number | null + newCategoryName?: string | null + newCategoryIcon?: string | null + tagIds?: number[] + newTags?: AiNewTag[] +}[]) { + return request.post('/ai/rule/apply', { rules }) +} + /** 一键应用建议:覆盖分类并追加标签,标记为新建的分类/标签会先创建再关联 */ export function applyAiSuggestions(suggestions: { transactionId: number @@ -111,7 +182,7 @@ export interface AiQueryResult { categoryIds: number[] categoryNames: string[] uncategorized: boolean - paymentMethods: string[] + channels: string[] startTime: string | null endTime: string | null includeInStats: boolean | null @@ -135,7 +206,7 @@ export interface AiRecordResult { amount: number | null categoryId: number | null categoryName: string | null - paymentMethod: string | null + channel: string | null transactionTime: string | null remark: string | null includeInStats: boolean | null diff --git a/frontend/src/api/app.ts b/frontend/src/api/app.ts new file mode 100644 index 0000000..dde918d --- /dev/null +++ b/frontend/src/api/app.ts @@ -0,0 +1,12 @@ +// 应用级配置接口:按账单来源的「计入统计」默认值 +import request from './request' + +/** 查询按账单来源的「计入统计」默认值(key 为账单来源编码) */ +export function getBillSourceIncludeInStats() { + return request.get('/app/bill-source-include-in-stats') +} + +/** 更新按账单来源的「计入统计」默认值 */ +export function updateBillSourceIncludeInStats(defaults: Record) { + return request.put('/app/bill-source-include-in-stats', { defaults }) +} diff --git a/frontend/src/api/rule.ts b/frontend/src/api/rule.ts index dd0a0d9..960b26a 100644 --- a/frontend/src/api/rule.ts +++ b/frontend/src/api/rule.ts @@ -39,11 +39,25 @@ export interface RulePayload { tagIds?: number[] } +/** 规则分页查询参数 */ +export interface RulePageParams { + page?: number + size?: number + keyword?: string + enabled?: boolean + type?: string +} + /** 获取规则列表 */ export function getRuleList() { return request.get('/rule/list') } +/** 分页查询规则(按名称/关键词、启用状态、收支类型筛选) */ +export function getRulePage(params: RulePageParams) { + return request.get('/rule/page', { params }) +} + /** 创建规则 */ export function createRule(data: RulePayload) { return request.post('/rule', data) diff --git a/frontend/src/api/statistics.ts b/frontend/src/api/statistics.ts index b0e90ee..ebe85ab 100644 --- a/frontend/src/api/statistics.ts +++ b/frontend/src/api/statistics.ts @@ -23,8 +23,9 @@ export interface StatisticsFilterParams { types?: string[] categoryIds?: number[] uncategorized?: boolean + tagIds?: number[] untagged?: boolean - paymentMethods?: string[] + channels?: string[] minAmount?: number maxAmount?: number remark?: string @@ -37,7 +38,7 @@ export function getStatisticsSummary(params: StatisticsFilterParams) { return request.get('/statistics/summary', { params }) } -/** 按维度获取分组统计数据(dimension:day/week/month/quarter/year/paymentMethod/category/type) */ +/** 按维度获取分组统计数据(dimension:day/week/month/quarter/year/channel/category/type) */ export function getStatisticsGroup(params: StatisticsFilterParams & { dimension: string }) { return request.get('/statistics/group', { params }) } diff --git a/frontend/src/api/transaction.ts b/frontend/src/api/transaction.ts index c5e2335..fbf1b49 100644 --- a/frontend/src/api/transaction.ts +++ b/frontend/src/api/transaction.ts @@ -1,19 +1,19 @@ // 收支记录相关 API 接口:交易记录的增删改查(分页) import request from './request' -/** 获取系统支持的支付方式编码列表(含账单导入来源与人工支付方式) */ -export function getPaymentMethods() { - return request.get('/transaction/payment-methods') +/** 获取系统支持的交易渠道编码列表(含账单导入来源与人工交易渠道) */ +export function getChannels() { + return request.get('/transaction/channels') } -/** 分页获取交易记录列表,支持按类型、分类、支付方式、时间范围、计入统计、金额区间多选筛选 */ +/** 分页获取交易记录列表,支持按类型、分类、交易渠道、时间范围、计入统计、金额区间多选筛选 */ export function getTransactionPage(params: { page?: number size?: number types?: string[] categoryIds?: number[] uncategorized?: boolean - paymentMethods?: string[] + channels?: string[] startTime?: string endTime?: string includeInStats?: boolean @@ -24,6 +24,7 @@ export function getTransactionPage(params: { sortOrder?: string tagIds?: number[] untagged?: boolean + tagged?: boolean }) { return request.get('/transaction/page', { params }) } @@ -33,7 +34,7 @@ export function createTransaction(data: { categoryId: number type: string amount: number - paymentMethod?: string + channel?: string remark?: string transactionTime: string includeInStats?: boolean @@ -47,7 +48,7 @@ export function updateTransaction(id: number, data: { categoryId: number | null type: string amount: number - paymentMethod?: string + channel?: string remark?: string transactionTime: string includeInStats?: boolean @@ -163,7 +164,7 @@ export function exportTransaction(params: { types?: string[] categoryIds?: number[] uncategorized?: boolean - paymentMethods?: string[] + channels?: string[] startTime?: string endTime?: string includeInStats?: boolean diff --git a/frontend/src/components/BillSourceStatsPanel.vue b/frontend/src/components/BillSourceStatsPanel.vue new file mode 100644 index 0000000..a7fbb3b --- /dev/null +++ b/frontend/src/components/BillSourceStatsPanel.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/frontend/src/locales/messages.ts b/frontend/src/locales/messages.ts index fa468cc..43a0894 100644 --- a/frontend/src/locales/messages.ts +++ b/frontend/src/locales/messages.ts @@ -28,11 +28,12 @@ export default { deleteSuccess: { zh: '删除成功', en: 'Deleted successfully', ja: '削除しました' }, deleteFailed: { zh: '删除失败', en: 'Failed to delete', ja: '削除に失敗しました' }, confirmDelete: { zh: '确认删除', en: 'Confirm Deletion', ja: '削除の確認' }, + refresh: { zh: '刷新', en: 'Refresh', ja: '更新' }, types: { expense: { zh: '支出', en: 'Expense', ja: '支出' }, income: { zh: '收入', en: 'Income', ja: '収入' }, }, - payment: { + channel: { wechat: { zh: '微信', en: 'WeChat', ja: 'WeChat' }, alipay: { zh: '支付宝', en: 'Alipay', ja: 'Alipay' }, cash: { zh: '现金', en: 'Cash', ja: '現金' }, @@ -135,9 +136,21 @@ export default { aiSection: { zh: 'AI 模型配置', en: 'AI Model', ja: 'AI モデル' }, viewUsage: { zh: '使用情况', en: 'Usage', ja: '利用状況' }, registrationSection: { zh: '注册设置', en: 'Registration', ja: '登録設定' }, + billSourceStatsSection: { zh: '账单来源统计默认值', en: 'Bill Source Stats Defaults', ja: '請求元の集計既定値' }, notificationSection: { zh: '定时通知', en: 'Scheduled Notifications', ja: '定期通知' }, }, + // 账单来源统计默认值 + billSourceStats: { + hint: { zh: '设置各账单来源导入时是否默认计入统计;不计入的记录仍会导入,但不参与统计', en: 'Set whether bills from each source count toward statistics by default. Excluded records are still imported but not counted.', ja: '各請求元の取り込み時に集計へ含めるかを設定します。除外した記録も取り込みますが集計対象外になります。' }, + included: { zh: '计入统计', en: 'Included', ja: '集計対象' }, + excluded: { zh: '不计入统计', en: 'Excluded', ja: '集計対象外' }, + save: { zh: '保存', en: 'Save', ja: '保存' }, + loadFailed: { zh: '加载账单来源统计默认值失败', en: 'Failed to load bill source stats defaults', ja: '請求元の集計既定値の読み込みに失敗しました' }, + saveSuccess: { zh: '账单来源统计默认值已保存', en: 'Bill source stats defaults saved', ja: '請求元の集計既定値を保存しました' }, + saveFailed: { zh: '保存账单来源统计默认值失败', en: 'Failed to save bill source stats defaults', ja: '請求元の集計既定値の保存に失敗しました' }, + }, + // 注册设置 registration: { statusOn: { zh: '已开启', en: 'Enabled', ja: '有効' }, @@ -295,7 +308,16 @@ export default { }, filterType: { zh: '类型', en: 'Type', ja: '種類' }, filterCategory: { zh: '分类', en: 'Category', ja: 'カテゴリ' }, - filterPaymentMethod: { zh: '支付方式', en: 'Payment Method', ja: '支払方法' }, + filterChannel: { zh: '交易渠道', en: 'Channel', ja: '取引チャネル' }, + filterTag: { zh: '标签', en: 'Tag', ja: 'タグ' }, + aiSearchPlaceholder: { zh: '用一句话描述统计需求,如「上个月餐饮花了多少」', en: 'Describe what to analyze, e.g. "How much did I spend on dining last month?"', ja: '集計したい内容を文章で入力(例:先月の飲食費は?)' }, + aiSearchButton: { zh: 'AI 搜索', en: 'AI Search', ja: 'AI 検索' }, + aiSearchEmpty: { zh: '请输入统计需求', en: 'Please enter a query', ja: '検索内容を入力してください' }, + aiSearchApplied: { zh: '已按 AI 解析结果更新统计', en: 'Statistics updated from AI parsing', ja: 'AI の解析結果で統計を更新しました' }, + aiSearchFailed: { zh: 'AI 统计搜索失败', en: 'AI statistics search failed', ja: 'AI 統計検索に失敗しました' }, + aiExample1: { zh: '上个月餐饮花了多少', en: 'How much on dining last month?', ja: '先月の飲食費は?' }, + aiExample2: { zh: '近半年交通支出趋势,按周看', en: 'Transport spending trend over the last 6 months, weekly', ja: '直近半年の交通費の推移を週別で' }, + aiExample3: { zh: '今年各类支出占比', en: 'Expense breakdown by category this year', ja: '今年のカテゴリ別支出割合' }, minAmount: { zh: '最小金额', en: 'Min Amount', ja: '最小金額' }, maxAmount: { zh: '最大金额', en: 'Max Amount', ja: '最大金額' }, remark: { zh: '备注搜索', en: 'Search Remark', ja: '備考検索' }, @@ -311,8 +333,8 @@ export default { trend: { zh: '收支趋势', en: 'Income & Expense Trend', ja: '収支の推移' }, expenseCategoryPie: { zh: '支出分类占比', en: 'Expense by Category', ja: '支出カテゴリの割合' }, incomeCategoryPie: { zh: '收入分类占比', en: 'Income by Category', ja: '収入カテゴリの割合' }, - expensePaymentPie: { zh: '支出支付方式占比', en: 'Expense by Payment Method', ja: '支出の支払方法の割合' }, - incomePaymentPie: { zh: '收入支付方式占比', en: 'Income by Payment Method', ja: '収入の支払方法の割合' }, + expenseChannelPie: { zh: '支出交易渠道占比', en: 'Expense by Channel', ja: '支出の取引チャネル割合' }, + incomeChannelPie: { zh: '收入交易渠道占比', en: 'Income by Channel', ja: '収入の取引チャネル割合' }, uncategorized: { zh: '未分类', en: 'Uncategorized', ja: '未分類' }, }, @@ -349,7 +371,7 @@ export default { type: { zh: '类型', en: 'Type', ja: '種類' }, category: { zh: '分类', en: 'Category', ja: 'カテゴリ' }, amount: { zh: '金额', en: 'Amount', ja: '金額' }, - paymentMethod: { zh: '支付方式', en: 'Payment', ja: '支払方法' }, + channel: { zh: '交易渠道', en: 'Channel', ja: '取引チャネル' }, remark: { zh: '备注', en: 'Remark', ja: '備考' }, tags: { zh: '标签', en: 'Tags', ja: 'タグ' }, includeInStats: { zh: '计入统计', en: 'In Stats', ja: '集計対象' }, @@ -391,7 +413,7 @@ export default { validAmount: { zh: '请输入有效金额', en: 'Please enter a valid amount', ja: '有効な金額を入力してください' }, selectCategoryPlaceholder: { zh: '请选择分类', en: 'Select a category', ja: 'カテゴリを選択' }, amountPlaceholder: { zh: '请输入金额', en: 'Enter amount', ja: '金額を入力してください' }, - paymentPlaceholder: { zh: '选择支付方式', en: 'Select payment method', ja: '支払方法を選択' }, + channelPlaceholder: { zh: '选择交易渠道', en: 'Select channel', ja: '取引チャネルを選択' }, remarkPlaceholder: { zh: '可选备注说明', en: 'Optional remark', ja: '備考(任意)' }, remarkHistoryClear: { zh: '清空历史', en: 'Clear history', ja: '履歴をクリア' }, tagPlaceholder: { zh: '选择标签', en: 'Select tags', ja: 'タグを選択' }, @@ -480,6 +502,10 @@ export default { deleteConfirm: { zh: '确定要删除这个规则吗?', en: 'Are you sure you want to delete this rule?', ja: 'このルールを削除してもよろしいですか?' }, loadCategoriesFailed: { zh: '加载分类失败', en: 'Failed to load categories', ja: 'カテゴリの読み込みに失敗しました' }, loadTagsFailed: { zh: '加载标签失败', en: 'Failed to load tags', ja: 'タグの読み込みに失敗しました' }, + searchPlaceholder: { zh: '搜索规则名称或关键词', en: 'Search name or keyword', ja: 'ルール名またはキーワードを検索' }, + status: { zh: '启用状态', en: 'Status', ja: '有効状態' }, + statusOn: { zh: '已启用', en: 'Enabled', ja: '有効' }, + statusOff: { zh: '已停用', en: 'Disabled', ja: '無効' }, }, // 账单导入 @@ -601,6 +627,28 @@ export default { nameRequired: { zh: '请先输入名称', en: 'Please enter a name first', ja: '先に名前を入力してください' }, failed: { zh: 'AI 推荐失败', en: 'AI recommendation failed', ja: 'AI の提案に失敗しました' }, }, + rule: { + entry: { zh: 'AI 生成规则', en: 'AI Generate Rules', ja: 'AI でルール生成' }, + title: { zh: 'AI 生成规则建议', en: 'AI Rule Suggestions', ja: 'AI ルール提案' }, + hint: { zh: 'AI 将依据所选记录的已有标签与分类,生成分类与标签规则建议,可编辑后勾选创建', en: 'AI derives rules from the tags and categories of the selected records; edit and select to create.', ja: 'AI が選択した記録の既存タグとカテゴリからルールを提案します。編集して選択後に作成できます。' }, + selectHint: { zh: '选择要用于生成规则的已打标签记录(默认全选当前页,已选 {count} 条)', en: 'Select tagged records to derive rules (current page selected by default, {count} selected)', ja: 'ルール生成に使うタグ付き記録を選択(既定で現在ページを全選択、選択中 {count} 件)' }, + searchPlaceholder: { zh: '搜索备注', en: 'Search remark', ja: '備考を検索' }, + selectPageAll: { zh: '全选本页', en: 'Select page', ja: 'このページを全選択' }, + clearSelection: { zh: '清空选择', en: 'Clear selection', ja: '選択をクリア' }, + generate: { zh: '生成规则', en: 'Generate rules', ja: 'ルールを生成' }, + back: { zh: '返回上一步', en: 'Back', ja: '戻る' }, + noTagged: { zh: '暂无可用的已打标签记录', en: 'No tagged records available', ja: 'タグ付きの記録がありません' }, + loadRecordsFailed: { zh: '加载已打标签记录失败', en: 'Failed to load tagged records', ja: 'タグ付き記録の読み込みに失敗しました' }, + reason: { zh: '理由', en: 'Reason', ja: '理由' }, + newPrefix: { zh: '新建:', en: 'New: ', ja: '新規:' }, + apply: { zh: '创建勾选规则', en: 'Create selected', ja: '選択したルールを作成' }, + selectRequired: { zh: '请至少勾选一条记录或规则', en: 'Please select at least one record or rule', ja: '少なくとも1件の記録またはルールを選択してください' }, + applied: { zh: '已创建 {count} 条规则', en: 'Created {count} rule(s)', ja: '{count} 件のルールを作成しました' }, + failed: { zh: 'AI 规则生成失败', en: 'Failed to generate AI rules', ja: 'AI ルールの生成に失敗しました' }, + applyFailed: { zh: '创建规则失败', en: 'Failed to create rules', ja: 'ルールの作成に失敗しました' }, + duplicate: { zh: '已存在', en: 'Exists', ja: '既存' }, + allDuplicate: { zh: '生成的规则均已存在,无需创建', en: 'All generated rules already exist', ja: '生成されたルールはすべて既存です' }, + }, category: { label: { zh: 'AI 生成分类', en: 'AI Category', ja: 'AI 生成カテゴリ' }, placeholder: { zh: '描述用途,如:日常通勤打车地铁', en: 'Describe it, e.g. daily commute by taxi and metro', ja: '用途を説明してください(例:日常の通勤でタクシーと地下鉄)' }, @@ -643,7 +691,7 @@ export default { type: { zh: '类型', en: 'Type', ja: '種類' }, category: { zh: '分类', en: 'Category', ja: 'カテゴリ' }, tag: { zh: '标签', en: 'Tag', ja: 'タグ' }, - payment: { zh: '支付方式', en: 'Payment', ja: '支払方法' }, + channel: { zh: '交易渠道', en: 'Channel', ja: '取引チャネル' }, dateRange: { zh: '时间范围', en: 'Date range', ja: '期間' }, amountRange: { zh: '金额范围', en: 'Amount range', ja: '金額範囲' }, remark: { zh: '备注', en: 'Remark', ja: '備考' }, diff --git a/frontend/src/utils/payment.ts b/frontend/src/utils/channel.ts similarity index 63% rename from frontend/src/utils/payment.ts rename to frontend/src/utils/channel.ts index 0f3a621..8230297 100644 --- a/frontend/src/utils/payment.ts +++ b/frontend/src/utils/channel.ts @@ -1,14 +1,14 @@ -// 支付方式相关工具:将后端返回的支付方式编码映射为当前语言标签 +// 交易渠道相关工具:将后端返回的交易渠道编码映射为当前语言标签 import i18n from '../locales' /** i18n 全局实例的翻译函数签名(避免动态 key 触发过深的类型推断) */ type Translate = (key: string) => string type HasKey = (key: string) => boolean -/** 将支付方式编码转换为当前语言的标签,无对应文案时回退为原始编码 */ -export function getPaymentLabel(val: string): string { +/** 将交易渠道编码转换为当前语言的标签,无对应文案时回退为原始编码 */ +export function getChannelLabel(val: string): string { if (!val) return '-' const { t, te } = i18n.global as unknown as { t: Translate; te: HasKey } - const key = `common.payment.${val.toLowerCase()}` + const key = `common.channel.${val.toLowerCase()}` return te(key) ? t(key) : val } diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index ae72f61..f185d3a 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -1,8 +1,9 @@ @@ -338,10 +737,51 @@ onMounted(() => {

{{ t('rule.title') }}

- {{ t('rule.add') }} + + {{ t('ai.rule.entry') }} + {{ t('rule.add') }} +
- + + + + + + + {{ t('transaction.search') }} + {{ t('transaction.clearAll') }} + + + + @@ -396,5 +836,110 @@ onMounted(() => { {{ t('common.confirm') }} + + + + + + + + + diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 0107aae..d637f28 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -1,8 +1,9 @@ @@ -1507,6 +1509,12 @@ onMounted(() => { {{ t('transaction.importBill') }} {{ t('transaction.exportExcel') }} + + + {{ t('common.refresh') }} +