diff --git a/Directory.Packages.props b/Directory.Packages.props
index 899235d..5a1cee4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -29,5 +29,8 @@
+
+
+
\ No newline at end of file
diff --git a/Repository/BaseRepository.cs b/Repository/BaseRepository.cs
index ee80096..b41d2bc 100644
--- a/Repository/BaseRepository.cs
+++ b/Repository/BaseRepository.cs
@@ -16,6 +16,11 @@ public interface IBaseRepository where T : BaseEntity
///
Task GetByIdAsync(long id);
+ ///
+ /// 根据ID获取单条数据
+ ///
+ Task GetByIdsAsync(long[] ids);
+
///
/// 添加数据
///
@@ -76,6 +81,19 @@ public abstract class BaseRepository(IFreeSql freeSql) : IBaseRepository w
}
}
+ public virtual async Task GetByIdsAsync(long[] ids)
+ {
+ try
+ {
+ var result = await FreeSql.Select().Where(x => ids.Contains(x.Id)).ToListAsync();
+ return result.ToArray();
+ }
+ catch
+ {
+ return [];
+ }
+ }
+
public virtual async Task AddAsync(T entity)
{
try
diff --git a/Repository/TransactionRecordRepository.cs b/Repository/TransactionRecordRepository.cs
index 85defe7..ca5634a 100644
--- a/Repository/TransactionRecordRepository.cs
+++ b/Repository/TransactionRecordRepository.cs
@@ -149,6 +149,23 @@ public interface ITransactionRecordRepository : IBaseRepository完整的SELECT SQL语句
/// 动态查询结果列表
Task> ExecuteDynamicSqlAsync(string completeSql);
+
+ ///
+ /// 根据关键词查询已分类的账单(用于智能分类参考)
+ ///
+ /// 关键词列表
+ /// 返回结果数量限制
+ /// 已分类的账单列表
+ Task> GetClassifiedByKeywordsAsync(List keywords, int limit = 10);
+
+ ///
+ /// 根据关键词查询已分类的账单,并计算相关度分数
+ ///
+ /// 关键词列表
+ /// 最小匹配率(0.0-1.0),默认0.3表示至少匹配30%的关键词
+ /// 返回结果数量限制
+ /// 带相关度分数的已分类账单列表
+ Task> GetClassifiedByKeywordsWithScoreAsync(List keywords, double minMatchRate = 0.3, int limit = 10);
}
public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository(freeSql), ITransactionRecordRepository
@@ -344,7 +361,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20)
{
- // 先按照Reason分组,统计每个Reason的数量
+ // 先按照Reason分组,统计每个Reason的数量和总金额
var groups = await FreeSql.Select()
.Where(t => !string.IsNullOrEmpty(t.Reason))
.Where(t => string.IsNullOrEmpty(t.Classify)) // 只统计未分类的
@@ -352,11 +369,12 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository new
{
Reason = g.Key,
- Count = g.Count()
+ Count = g.Count(),
+ TotalAmount = g.Sum(g.Value.Amount)
});
- // 按数量降序排序
- var sortedGroups = groups.OrderByDescending(g => g.Count).ToList();
+ // 按总金额绝对值降序排序
+ var sortedGroups = groups.OrderByDescending(g => Math.Abs(g.TotalAmount)).ToList();
var total = sortedGroups.Count;
// 分页
@@ -365,22 +383,27 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository();
foreach (var group in pagedGroups)
{
- var sample = await FreeSql.Select()
+ // 获取该分组的所有记录
+ var records = await FreeSql.Select()
.Where(t => t.Reason == group.Reason)
- .FirstAsync();
+ .Where(t => string.IsNullOrEmpty(t.Classify))
+ .ToListAsync();
- if (sample != null)
+ if (records.Count > 0)
{
+ var sample = records.First();
result.Add(new ReasonGroupDto
{
Reason = group.Reason,
Count = (int)group.Count,
SampleType = sample.Type,
- SampleClassify = sample.Classify ?? string.Empty
+ SampleClassify = sample.Classify ?? string.Empty,
+ TransactionIds = records.Select(r => r.Id).ToList(),
+ TotalAmount = Math.Abs(group.TotalAmount)
});
}
}
@@ -542,6 +565,68 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository> GetClassifiedByKeywordsAsync(List keywords, int limit = 10)
+ {
+ if (keywords == null || keywords.Count == 0)
+ {
+ return new List();
+ }
+
+ var query = FreeSql.Select()
+ .Where(t => t.Classify != ""); // 只查询已分类的账单
+
+ // 构建OR条件:Reason包含任意一个关键词
+ if (keywords.Count > 0)
+ {
+ query = query.Where(t => keywords.Any(keyword => t.Reason.Contains(keyword)));
+ }
+
+ return await query
+ .OrderByDescending(t => t.OccurredAt)
+ .Limit(limit)
+ .ToListAsync();
+ }
+
+ public async Task> GetClassifiedByKeywordsWithScoreAsync(List keywords, double minMatchRate = 0.3, int limit = 10)
+ {
+ if (keywords == null || keywords.Count == 0)
+ {
+ return new List<(TransactionRecord, double)>();
+ }
+
+ // 查询所有已分类且包含任意关键词的账单
+ var candidates = await FreeSql.Select()
+ .Where(t => t.Classify != "")
+ .Where(t => keywords.Any(keyword => t.Reason.Contains(keyword)))
+ .ToListAsync();
+
+ // 计算每个候选账单的相关度分数
+ var scoredResults = candidates
+ .Select(record =>
+ {
+ var matchedCount = keywords.Count(keyword => record.Reason.Contains(keyword, StringComparison.OrdinalIgnoreCase));
+ var matchRate = (double)matchedCount / keywords.Count;
+
+ // 额外加分:完全匹配整个摘要(相似度更高)
+ var exactMatchBonus = keywords.Any(k => record.Reason.Equals(k, StringComparison.OrdinalIgnoreCase)) ? 0.2 : 0.0;
+
+ // 长度相似度加分:长度越接近,相关度越高
+ var avgKeywordLength = keywords.Average(k => k.Length);
+ var lengthSimilarity = 1.0 - Math.Min(1.0, Math.Abs(record.Reason.Length - avgKeywordLength) / Math.Max(record.Reason.Length, avgKeywordLength));
+ var lengthBonus = lengthSimilarity * 0.1;
+
+ var score = matchRate + exactMatchBonus + lengthBonus;
+ return (record, score);
+ })
+ .Where(x => x.score >= minMatchRate) // 过滤低相关度结果
+ .OrderByDescending(x => x.score) // 按相关度降序
+ .ThenByDescending(x => x.record.OccurredAt) // 相同分数时,按时间降序
+ .Take(limit)
+ .ToList();
+
+ return scoredResults;
+ }
}
///
@@ -568,6 +653,16 @@ public class ReasonGroupDto
/// 示例分类(该分组中第一条记录的分类)
///
public string SampleClassify { get; set; } = string.Empty;
+
+ ///
+ /// 该分组的所有账单ID列表
+ ///
+ public List TransactionIds { get; set; } = new();
+
+ ///
+ /// 该分组的总金额(绝对值)
+ ///
+ public decimal TotalAmount { get; set; }
}
///
diff --git a/Service/Service.csproj b/Service/Service.csproj
index bbfa04e..a2bf00f 100644
--- a/Service/Service.csproj
+++ b/Service/Service.csproj
@@ -19,6 +19,8 @@
+
+
diff --git a/Service/TextSegmentService.cs b/Service/TextSegmentService.cs
new file mode 100644
index 0000000..0453156
--- /dev/null
+++ b/Service/TextSegmentService.cs
@@ -0,0 +1,152 @@
+namespace Service;
+
+using JiebaNet.Segmenter;
+using JiebaNet.Analyser;
+using Microsoft.Extensions.Logging;
+
+///
+/// 文本分词服务接口
+///
+public interface ITextSegmentService
+{
+ ///
+ /// 从文本中提取关键词
+ ///
+ /// 待分析的文本
+ /// 返回前N个关键词,默认5个
+ /// 关键词列表
+ List ExtractKeywords(string text, int topN = 5);
+
+ ///
+ /// 对文本进行分词
+ ///
+ /// 待分词的文本
+ /// 分词结果列表
+ List Segment(string text);
+}
+
+///
+/// 基于 JiebaNet 的文本分词服务实现
+///
+public class TextSegmentService : ITextSegmentService
+{
+ private readonly JiebaSegmenter _segmenter;
+ private readonly TfidfExtractor _extractor;
+ private readonly ILogger _logger;
+
+ public TextSegmentService(ILogger logger)
+ {
+ _logger = logger;
+ _segmenter = new JiebaSegmenter();
+ _extractor = new TfidfExtractor();
+
+ // 仅添加JiebaNet词典中可能缺失的特定业务词汇
+ AddCustomWords();
+ }
+
+ ///
+ /// 添加自定义词典 - 仅添加JiebaNet词典中可能缺失的特定词汇
+ ///
+ private void AddCustomWords()
+ {
+ try
+ {
+ // 只添加可能缺失的特定业务词汇
+ // 大部分常用词(如"美团"、"支付宝"等)JiebaNet已内置
+ var customWords = new[]
+ {
+ "水电费", "物业费", "燃气费" // 复合词,确保作为整体识别 // TODO 做成配置文件 让 AI定期提取复合词汇填入到这边
+ };
+
+ foreach (var word in customWords)
+ {
+ _segmenter.AddWord(word);
+ }
+
+ if (customWords.Length > 0)
+ {
+ _logger.LogDebug("已加载 {Count} 个自定义词汇", customWords.Length);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "添加自定义词典失败");
+ }
+ }
+
+ public List ExtractKeywords(string text, int topN = 5)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ return new List();
+ }
+
+ try
+ {
+ // 使用 TF-IDF 算法提取关键词(已内置停用词过滤)
+ var keywords = _extractor.ExtractTags(text, topN, new List());
+
+ // 过滤单字,保留有意义的词
+ var filteredKeywords = keywords
+ .Where(k => k.Length >= 2)
+ .Distinct()
+ .ToList();
+
+ // 如果过滤后没有关键词,使用基础分词并选择最长的词
+ if (filteredKeywords.Count == 0)
+ {
+ var segments = Segment(text);
+ filteredKeywords = segments
+ .Where(s => s.Length >= 2)
+ .OrderByDescending(s => s.Length)
+ .Take(topN)
+ .Distinct()
+ .ToList();
+ }
+
+ // 如果还是没有,返回原文的前10个字符
+ if (filteredKeywords.Count == 0 && text.Length > 0)
+ {
+ filteredKeywords.Add(text.Length > 10 ? text.Substring(0, 10) : text);
+ }
+
+ _logger.LogDebug("从文本 '{Text}' 中提取关键词: {Keywords}",
+ text, string.Join(", ", filteredKeywords));
+
+ return filteredKeywords;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "提取关键词失败,文本: {Text}", text);
+ // 降级处理:返回原文
+ return new List { text.Length > 10 ? text.Substring(0, 10) : text };
+ }
+ }
+
+ public List Segment(string text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ return new List();
+ }
+
+ try
+ {
+ // 执行分词
+ var segments = _segmenter.Cut(text).ToList();
+
+ // 过滤空白和停用词
+ var filteredSegments = segments
+ .Where(s => !string.IsNullOrWhiteSpace(s) && s.Trim().Length > 0)
+ .Select(s => s.Trim())
+ .ToList();
+
+ return filteredSegments;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "分词失败,文本: {Text}", text);
+ return new List { text };
+ }
+ }
+}
diff --git a/Web/.eslintcache b/Web/.eslintcache
new file mode 100644
index 0000000..dac762e
--- /dev/null
+++ b/Web/.eslintcache
@@ -0,0 +1 @@
+[{"D:\\codes\\others\\EmailBill\\Web\\src\\views\\ClassificationSmart.vue":"1","D:\\codes\\others\\EmailBill\\Web\\eslint.config.js":"2","D:\\codes\\others\\EmailBill\\Web\\public\\service-worker.js":"3","D:\\codes\\others\\EmailBill\\Web\\src\\api\\billImport.js":"4","D:\\codes\\others\\EmailBill\\Web\\src\\api\\emailRecord.js":"5","D:\\codes\\others\\EmailBill\\Web\\src\\api\\log.js":"6","D:\\codes\\others\\EmailBill\\Web\\src\\api\\message.js":"7","D:\\codes\\others\\EmailBill\\Web\\src\\api\\request.js":"8","D:\\codes\\others\\EmailBill\\Web\\src\\api\\statistics.js":"9","D:\\codes\\others\\EmailBill\\Web\\src\\api\\transactionCategory.js":"10","D:\\codes\\others\\EmailBill\\Web\\src\\api\\transactionPeriodic.js":"11","D:\\codes\\others\\EmailBill\\Web\\src\\api\\transactionRecord.js":"12","D:\\codes\\others\\EmailBill\\Web\\src\\App.vue":"13","D:\\codes\\others\\EmailBill\\Web\\src\\components\\ClassifyPicker.vue":"14","D:\\codes\\others\\EmailBill\\Web\\src\\components\\TransactionDetail.vue":"15","D:\\codes\\others\\EmailBill\\Web\\src\\components\\TransactionList.vue":"16","D:\\codes\\others\\EmailBill\\Web\\src\\main.js":"17","D:\\codes\\others\\EmailBill\\Web\\src\\registerServiceWorker.js":"18","D:\\codes\\others\\EmailBill\\Web\\src\\router\\index.js":"19","D:\\codes\\others\\EmailBill\\Web\\src\\stores\\auth.js":"20","D:\\codes\\others\\EmailBill\\Web\\src\\stores\\counter.js":"21","D:\\codes\\others\\EmailBill\\Web\\src\\stores\\message.js":"22","D:\\codes\\others\\EmailBill\\Web\\src\\views\\BalanceView.vue":"23","D:\\codes\\others\\EmailBill\\Web\\src\\views\\BillAnalysisView.vue":"24","D:\\codes\\others\\EmailBill\\Web\\src\\views\\CalendarView.vue":"25","D:\\codes\\others\\EmailBill\\Web\\src\\views\\ClassificationBatch.vue":"26","D:\\codes\\others\\EmailBill\\Web\\src\\views\\ClassificationEdit.vue":"27","D:\\codes\\others\\EmailBill\\Web\\src\\views\\ClassificationNLP.vue":"28","D:\\codes\\others\\EmailBill\\Web\\src\\views\\EmailRecord.vue":"29","D:\\codes\\others\\EmailBill\\Web\\src\\views\\LoginView.vue":"30","D:\\codes\\others\\EmailBill\\Web\\src\\views\\LogView.vue":"31","D:\\codes\\others\\EmailBill\\Web\\src\\views\\MessageView.vue":"32","D:\\codes\\others\\EmailBill\\Web\\src\\views\\PeriodicRecord.vue":"33","D:\\codes\\others\\EmailBill\\Web\\src\\views\\SettingView.vue":"34","D:\\codes\\others\\EmailBill\\Web\\src\\views\\StatisticsView.vue":"35","D:\\codes\\others\\EmailBill\\Web\\src\\views\\TransactionsRecord.vue":"36","D:\\codes\\others\\EmailBill\\Web\\vite.config.js":"37"},{"size":12129,"mtime":1766998646236,"results":"38","hashOfConfig":"39"},{"size":596,"mtime":1766397301264,"results":"40","hashOfConfig":"41"},{"size":3763,"mtime":1766643343649,"results":"42","hashOfConfig":"41"},{"size":1786,"mtime":1766493393933,"results":"43","hashOfConfig":"41"},{"size":1828,"mtime":1766628922810,"results":"44","hashOfConfig":"41"},{"size":868,"mtime":1766997952394,"results":"45","hashOfConfig":"41"},{"size":1670,"mtime":1766988985063,"results":"46","hashOfConfig":"41"},{"size":2280,"mtime":1766737267237,"results":"47","hashOfConfig":"41"},{"size":2968,"mtime":1766740438323,"results":"48","hashOfConfig":"41"},{"size":1916,"mtime":1766716056978,"results":"49","hashOfConfig":"41"},{"size":2855,"mtime":1766991229153,"results":"50","hashOfConfig":"41"},{"size":5791,"mtime":1766725491308,"results":"51","hashOfConfig":"41"},{"size":4370,"mtime":1766988985057,"results":"52","hashOfConfig":"39"},{"size":4162,"mtime":1766991229152,"results":"53","hashOfConfig":"39"},{"size":8877,"mtime":1766995651564,"results":"54","hashOfConfig":"39"},{"size":8006,"mtime":1766732771392,"results":"55","hashOfConfig":"39"},{"size":562,"mtime":1766971736444,"results":"56","hashOfConfig":"41"},{"size":2903,"mtime":1766998608620,"results":"57","hashOfConfig":"41"},{"size":3069,"mtime":1766997952394,"results":"58","hashOfConfig":"41"},{"size":1345,"mtime":1766640160651,"results":"59","hashOfConfig":"41"},{"size":306,"mtime":1766397238501,"results":"60","hashOfConfig":"41"},{"size":529,"mtime":1766988985056,"results":"61","hashOfConfig":"41"},{"size":1498,"mtime":1766971736456,"results":"62","hashOfConfig":"39"},{"size":7872,"mtime":1766971736459,"results":"63","hashOfConfig":"39"},{"size":7374,"mtime":1766995651564,"results":"64","hashOfConfig":"39"},{"size":12825,"mtime":1766971736468,"results":"65","hashOfConfig":"39"},{"size":6993,"mtime":1766993465076,"results":"66","hashOfConfig":"39"},{"size":8753,"mtime":1766995651587,"results":"67","hashOfConfig":"39"},{"size":12489,"mtime":1766995651561,"results":"68","hashOfConfig":"39"},{"size":2145,"mtime":1766978070072,"results":"69","hashOfConfig":"39"},{"size":9634,"mtime":1766997952394,"results":"70","hashOfConfig":"39"},{"size":6774,"mtime":1766995651567,"results":"71","hashOfConfig":"39"},{"size":22374,"mtime":1766995651561,"results":"72","hashOfConfig":"39"},{"size":5264,"mtime":1766997952404,"results":"73","hashOfConfig":"39"},{"size":28355,"mtime":1766995651561,"results":"74","hashOfConfig":"39"},{"size":15032,"mtime":1766995651561,"results":"75","hashOfConfig":"39"},{"size":702,"mtime":1766737248019,"results":"76","hashOfConfig":"41"},{"filePath":"77","messages":"78","suppressedMessages":"79","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1xefihw",{"filePath":"80","messages":"81","suppressedMessages":"82","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"19tzkw5",{"filePath":"83","messages":"84","suppressedMessages":"85","errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"86","messages":"87","suppressedMessages":"88","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"89","messages":"90","suppressedMessages":"91","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"92","messages":"93","suppressedMessages":"94","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"95","messages":"96","suppressedMessages":"97","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"98","messages":"99","suppressedMessages":"100","errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"101","messages":"102","suppressedMessages":"103","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"104","messages":"105","suppressedMessages":"106","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"107","messages":"108","suppressedMessages":"109","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"110","messages":"111","suppressedMessages":"112","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"113","messages":"114","suppressedMessages":"115","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"116","messages":"117","suppressedMessages":"118","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"119","messages":"120","suppressedMessages":"121","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"122","messages":"123","suppressedMessages":"124","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"125","messages":"126","suppressedMessages":"127","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"128","messages":"129","suppressedMessages":"130","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"131","messages":"132","suppressedMessages":"133","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"134","messages":"135","suppressedMessages":"136","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"137","messages":"138","suppressedMessages":"139","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"140","messages":"141","suppressedMessages":"142","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"143","messages":"144","suppressedMessages":"145","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"146","messages":"147","suppressedMessages":"148","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"149","messages":"150","suppressedMessages":"151","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"152","messages":"153","suppressedMessages":"154","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"155","messages":"156","suppressedMessages":"157","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"158","messages":"159","suppressedMessages":"160","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"161","messages":"162","suppressedMessages":"163","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"164","messages":"165","suppressedMessages":"166","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"167","messages":"168","suppressedMessages":"169","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"170","messages":"171","suppressedMessages":"172","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"173","messages":"174","suppressedMessages":"175","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"176","messages":"177","suppressedMessages":"178","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"179","messages":"180","suppressedMessages":"181","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"182","messages":"183","suppressedMessages":"184","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"185","messages":"186","suppressedMessages":"187","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"D:\\codes\\others\\EmailBill\\Web\\src\\views\\ClassificationSmart.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\eslint.config.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\public\\service-worker.js",["188"],[],"D:\\codes\\others\\EmailBill\\Web\\src\\api\\billImport.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\api\\emailRecord.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\api\\log.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\api\\message.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\api\\request.js",["189"],[],"D:\\codes\\others\\EmailBill\\Web\\src\\api\\statistics.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\api\\transactionCategory.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\api\\transactionPeriodic.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\api\\transactionRecord.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\App.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\components\\ClassifyPicker.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\components\\TransactionDetail.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\components\\TransactionList.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\main.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\registerServiceWorker.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\router\\index.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\stores\\auth.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\stores\\counter.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\stores\\message.js",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\BalanceView.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\BillAnalysisView.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\CalendarView.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\ClassificationBatch.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\ClassificationEdit.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\ClassificationNLP.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\EmailRecord.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\LoginView.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\LogView.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\MessageView.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\PeriodicRecord.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\SettingView.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\StatisticsView.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\src\\views\\TransactionsRecord.vue",[],[],"D:\\codes\\others\\EmailBill\\Web\\vite.config.js",[],[],{"ruleId":"190","severity":2,"message":"191","line":129,"column":5,"nodeType":"192","messageId":"193","endLine":129,"endColumn":12},{"ruleId":"194","severity":2,"message":"195","line":59,"column":11,"nodeType":"196","messageId":"197","endLine":59,"endColumn":43,"suggestions":"198"},"no-undef","'clients' is not defined.","Identifier","undef","no-case-declarations","Unexpected lexical declaration in case block.","VariableDeclaration","unexpected",["199"],{"messageId":"200","fix":"201","desc":"202"},"addBrackets",{"range":"203","text":"204"},"Add {} brackets around the case block.",[1274,1513],"{ message = '未授权,请重新登录'\r\n // 清除登录状态并跳转到登录页\r\n const authStore = useAuthStore()\r\n authStore.logout()\r\n router.push({ name: 'login', query: { redirect: router.currentRoute.value.fullPath } })\r\n break }"]
\ No newline at end of file
diff --git a/Web/src/components/SmartClassifyButton.vue b/Web/src/components/SmartClassifyButton.vue
new file mode 100644
index 0000000..3158693
--- /dev/null
+++ b/Web/src/components/SmartClassifyButton.vue
@@ -0,0 +1,287 @@
+
+
+
+
+ {{ hasClassifiedResults ? '保存分类' : '智能分类' }}
+
+
+ {{ saving ? '保存中...' : '分类中...' }}
+
+
+
+
+
+
+
diff --git a/Web/src/registerServiceWorker.js b/Web/src/registerServiceWorker.js
index 7c09967..aae2477 100644
--- a/Web/src/registerServiceWorker.js
+++ b/Web/src/registerServiceWorker.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-console */
+
export function register() {
if ('serviceWorker' in navigator) {
diff --git a/Web/src/views/CalendarView.vue b/Web/src/views/CalendarView.vue
index e7ca570..bbee147 100644
--- a/Web/src/views/CalendarView.vue
+++ b/Web/src/views/CalendarView.vue
@@ -17,15 +17,26 @@
position="bottom"
:style="{ height: '85%' }"
round
- closeable
>