diff --git a/Repository/TransactionRecordRepository.cs b/Repository/TransactionRecordRepository.cs index a283e10..f0d803e 100644 --- a/Repository/TransactionRecordRepository.cs +++ b/Repository/TransactionRecordRepository.cs @@ -169,6 +169,15 @@ public interface ITransactionRecordRepository : IBaseRepository返回结果数量限制 /// 带相关度分数的已分类账单列表 Task> GetClassifiedByKeywordsWithScoreAsync(List keywords, double minMatchRate = 0.3, int limit = 10); + + /// + /// 获取抵账候选列表 + /// + /// 当前交易ID + /// 当前交易金额 + /// 当前交易类型 + /// 候选交易列表 + Task> GetCandidatesForOffsetAsync(long currentId, decimal amount, TransactionType currentType); } public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository(freeSql), ITransactionRecordRepository @@ -636,6 +645,22 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository> GetCandidatesForOffsetAsync(long currentId, decimal amount, TransactionType currentType) + { + var absAmount = Math.Abs(amount); + var minAmount = absAmount - 5; + var maxAmount = absAmount + 5; + + var list = await FreeSql.Select() + .Where(t => t.Id != currentId) + .Where(t => t.Type != currentType) + .Where(t => Math.Abs(t.Amount) >= minAmount && Math.Abs(t.Amount) <= maxAmount) + .Take(50) + .ToListAsync(); + + return list.OrderBy(t => Math.Abs(Math.Abs(t.Amount) - absAmount)).ToList(); + } } /// diff --git a/Service/SmartHandleService.cs b/Service/SmartHandleService.cs index 286d154..4586cea 100644 --- a/Service/SmartHandleService.cs +++ b/Service/SmartHandleService.cs @@ -5,6 +5,8 @@ public interface ISmartHandleService Task SmartClassifyAsync(long[] transactionIds, Action<(string type, string data)> chunkAction); Task AnalyzeBillAsync(string userInput, Action chunkAction); + + Task ParseOneLineBillAsync(string text); } public class SmartHandleService( @@ -419,6 +421,41 @@ public class SmartHandleService( _ => "未知" }; } + + public async Task ParseOneLineBillAsync(string text) + { + // 获取所有分类 + var categories = await categoryRepository.GetAllAsync(); + var categoryList = string.Join("、", categories.Select(c => $"{GetTypeName(c.Type)}-{c.Name}")); + + var sysPrompt = $""" + 你是一个智能账单解析助手。请从用户提供的文本中提取交易信息,包括日期、金额、摘要、类型和分类。 + + 请返回 JSON 格式,包含以下字段: + - OccurredAt: 日期时间,格式 yyyy-MM-dd HH:mm:ss。当前系统时间为{DateTime.Now:yyyy-MM-dd HH:mm:ss}。 + - Amount: 金额,数字。 + - Reason: 备注/摘要,原文或其他补充信息。 + - Type: 交易类型,0=支出,1=收入,2=不计入收支。根据语义判断。 + - Classify: 分类,请从以下现有分类中选择最匹配的一个:{categoryList}。如果无法匹配,请返回""其他""。 + + 只返回 JSON,不要包含 markdown 标记。 + """; + var json = await openAiService.ChatAsync(sysPrompt, text); + if (string.IsNullOrWhiteSpace(json)) return null; + + try + { + // 清理可能的 markdown 标记 + json = json.Replace("```json", "").Replace("```", "").Trim(); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + return JsonSerializer.Deserialize(json, options); + } + catch (Exception ex) + { + logger.LogError(ex, "解析账单失败"); + return null; + } + } } /// @@ -435,3 +472,5 @@ public record GroupClassifyResult [JsonPropertyName("type")] public TransactionType Type { get; set; } } + +public record TransactionParseResult(string OccurredAt, string Classify, decimal Amount, string Reason, TransactionType Type); \ No newline at end of file diff --git a/Web/package.json b/Web/package.json index f390d67..fd78880 100644 --- a/Web/package.json +++ b/Web/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "axios": "^1.13.2", + "dayjs": "^1.11.19", "pinia": "^3.0.4", "vant": "^4.9.22", "vue": "^3.5.25", diff --git a/Web/pnpm-lock.yaml b/Web/pnpm-lock.yaml index 95f352b..9b16b48 100644 --- a/Web/pnpm-lock.yaml +++ b/Web/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: axios: specifier: ^1.13.2 version: 1.13.2 + dayjs: + specifier: ^1.11.19 + version: 1.11.19 pinia: specifier: ^3.0.4 version: 3.0.4(vue@3.5.26) @@ -749,6 +752,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2100,6 +2106,8 @@ snapshots: csstype@3.2.3: {} + dayjs@1.11.19: {} + debug@4.4.3: dependencies: ms: 2.1.3 diff --git a/Web/src/App.vue b/Web/src/App.vue index c624a12..c0334e2 100644 --- a/Web/src/App.vue +++ b/Web/src/App.vue @@ -19,6 +19,7 @@ 设置 + @@ -27,6 +28,7 @@ import { RouterView, useRoute } from 'vue-router' import { ref, onMounted, onUnmounted, computed, watch } from 'vue' import { useMessageStore } from '@/stores/message' +import GlobalAddBill from '@/components/Global/GlobalAddBill.vue' import '@/styles/common.css' const messageStore = useMessageStore() @@ -119,6 +121,12 @@ const handleTabClick = (path) => { } } +const handleAddTransactionSuccess = () => { + // 当添加交易成功时,通知当前页面刷新数据 + const event = new Event('transactions-changed') + window.dispatchEvent(event) +} + diff --git a/Web/src/components/Bill/ManualBillAdd.vue b/Web/src/components/Bill/ManualBillAdd.vue new file mode 100644 index 0000000..5789bae --- /dev/null +++ b/Web/src/components/Bill/ManualBillAdd.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/Web/src/components/Bill/OneLineBillAdd.vue b/Web/src/components/Bill/OneLineBillAdd.vue new file mode 100644 index 0000000..6ce96c9 --- /dev/null +++ b/Web/src/components/Bill/OneLineBillAdd.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/Web/src/components/Global/GlobalAddBill.vue b/Web/src/components/Global/GlobalAddBill.vue new file mode 100644 index 0000000..709df66 --- /dev/null +++ b/Web/src/components/Global/GlobalAddBill.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/Web/src/components/PopupContainer.vue b/Web/src/components/PopupContainer.vue index bb2cd3f..964b6a1 100644 --- a/Web/src/components/PopupContainer.vue +++ b/Web/src/components/PopupContainer.vue @@ -9,13 +9,20 @@