fix
This commit is contained in:
@@ -123,10 +123,13 @@ public class SmartHandleService(
|
|||||||
3. 如果无法确定分类,可以选择"其他"
|
3. 如果无法确定分类,可以选择"其他"
|
||||||
4. 每个分组可能包含多条账单,你需要为整个分组选择一个分类
|
4. 每个分组可能包含多条账单,你需要为整个分组选择一个分类
|
||||||
|
|
||||||
请对每个分组进行分类,每次输出一个分组的分类结果,格式如下:
|
输出格式要求(强制):
|
||||||
{"reason": "交易摘要", "type": 0:支出/1:收入/2:不计入收支(Type为Number枚举值) ,"classify": "分类名称"}
|
- 请使用 NDJSON(每行一个独立的 JSON 对象,末尾以换行符分隔),不要输出数组。
|
||||||
|
- 每行的JSON格式严格为:{"reason": "交易摘要", "type": 0, "classify": "分类名称"}
|
||||||
|
- 不要输出任何解释性文字、编号、标点或多余的文本
|
||||||
|
- 如果无法判断分类,请将 "classify" 设为 "其他",并确保仍然输出 JSON 行
|
||||||
|
|
||||||
只输出JSON,不要有其他文字说明。
|
只输出按行的JSON对象(NDJSON),不要有其他文字说明。
|
||||||
""";
|
""";
|
||||||
|
|
||||||
var userPrompt = $$"""
|
var userPrompt = $$"""
|
||||||
@@ -140,60 +143,102 @@ public class SmartHandleService(
|
|||||||
// 流式调用AI
|
// 流式调用AI
|
||||||
chunkAction(("start", $"开始分类,共 {sampleRecords.Length} 条账单"));
|
chunkAction(("start", $"开始分类,共 {sampleRecords.Length} 条账单"));
|
||||||
|
|
||||||
// 用于存储AI返回的分组分类结果
|
|
||||||
var classifyResults = new List<(string Reason, string Classify, TransactionType Type)>();
|
var classifyResults = new List<(string Reason, string Classify, TransactionType Type)>();
|
||||||
var buffer = new StringBuilder();
|
|
||||||
var sendedIds = new HashSet<long>();
|
var sendedIds = new HashSet<long>();
|
||||||
await foreach (var chunk in openAiService.ChatStreamAsync(systemPrompt, userPrompt))
|
|
||||||
{
|
|
||||||
buffer.Append(chunk);
|
|
||||||
|
|
||||||
// 尝试解析完整的JSON对象
|
// 将流解析逻辑提取为本地函数以减少嵌套
|
||||||
var bufferStr = buffer.ToString();
|
void HandleResult(GroupClassifyResult? result)
|
||||||
var startIdx = 0;
|
|
||||||
while (startIdx < bufferStr.Length)
|
|
||||||
{
|
{
|
||||||
var openBrace = bufferStr.IndexOf('{', startIdx);
|
if (result is null || string.IsNullOrEmpty(result.Reason)) return;
|
||||||
if (openBrace == -1) break;
|
classifyResults.Add((result.Reason, result.Classify ?? string.Empty, result.Type));
|
||||||
|
|
||||||
var closeBrace = FindMatchingBrace(bufferStr, openBrace);
|
|
||||||
if (closeBrace == -1) break;
|
|
||||||
|
|
||||||
var jsonStr = bufferStr.Substring(openBrace, closeBrace - openBrace + 1);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = JsonSerializer.Deserialize<GroupClassifyResult>(jsonStr);
|
|
||||||
if (result != null && !string.IsNullOrEmpty(result.Reason))
|
|
||||||
{
|
|
||||||
classifyResults.Add((result.Reason, result.Classify ?? "", result.Type));
|
|
||||||
// 每一条结果单独通知
|
|
||||||
var group = groupedRecords.FirstOrDefault(g => g.Reason == result.Reason);
|
var group = groupedRecords.FirstOrDefault(g => g.Reason == result.Reason);
|
||||||
if (group != null)
|
if (group == null) return;
|
||||||
{
|
|
||||||
// 为该分组的所有账单ID返回分类结果
|
|
||||||
foreach (var id in group.Ids)
|
foreach (var id in group.Ids)
|
||||||
{
|
{
|
||||||
if (!sendedIds.Contains(id))
|
if (sendedIds.Add(id))
|
||||||
{
|
{
|
||||||
sendedIds.Add(id);
|
|
||||||
var resultJson = JsonSerializer.Serialize(new { id, result.Classify, result.Type });
|
var resultJson = JsonSerializer.Serialize(new { id, result.Classify, result.Type });
|
||||||
chunkAction(("data", resultJson));
|
chunkAction(("data", resultJson));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析缓冲区中的所有完整 JSON 对象或数组
|
||||||
|
void FlushBuffer(StringBuilder buffer)
|
||||||
|
{
|
||||||
|
var buf = buffer.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(buf)) return;
|
||||||
|
|
||||||
|
// 优先尝试解析完整数组
|
||||||
|
var trimmed = buf.TrimStart();
|
||||||
|
if (trimmed.Length > 0 && trimmed[0] == '[')
|
||||||
|
{
|
||||||
|
var lastArrEnd = buf.LastIndexOf(']');
|
||||||
|
if (lastArrEnd > -1)
|
||||||
|
{
|
||||||
|
var arrJson = buf.Substring(0, lastArrEnd + 1);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = JsonSerializer.Deserialize<GroupClassifyResult[]>(arrJson);
|
||||||
|
if (results != null)
|
||||||
|
{
|
||||||
|
foreach (var r in results) HandleResult(r);
|
||||||
}
|
}
|
||||||
|
buffer.Remove(0, lastArrEnd + 1);
|
||||||
|
buf = buffer.ToString();
|
||||||
|
}
|
||||||
|
catch (Exception exArr)
|
||||||
|
{
|
||||||
|
logger.LogDebug(exArr, "按数组解析AI返回失败,回退到逐对象解析。预览: {Preview}", arrJson?.Length > 200 ? arrJson.Substring(0, 200) + "..." : arrJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐对象解析
|
||||||
|
var startIdx = 0;
|
||||||
|
while (startIdx < buf.Length)
|
||||||
|
{
|
||||||
|
var openBrace = buf.IndexOf('{', startIdx);
|
||||||
|
if (openBrace == -1) break;
|
||||||
|
var closeBrace = FindMatchingBrace(buf, openBrace);
|
||||||
|
if (closeBrace == -1) break;
|
||||||
|
var jsonStr = buf.Substring(openBrace, closeBrace - openBrace + 1);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = JsonSerializer.Deserialize<GroupClassifyResult>(jsonStr);
|
||||||
|
HandleResult(result);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "解析AI分类结果失败: {JsonStr}", jsonStr);
|
logger.LogWarning(ex, "解析AI分类结果失败: {JsonStr}", jsonStr.Length > 200 ? jsonStr.Substring(0, 200) + "..." : jsonStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
startIdx = closeBrace + 1;
|
startIdx = closeBrace + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startIdx > 0)
|
||||||
|
{
|
||||||
|
buffer.Remove(0, startIdx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
await foreach (var chunk in openAiService.ChatStreamAsync(systemPrompt, userPrompt))
|
||||||
|
{
|
||||||
|
buffer.Append(chunk);
|
||||||
|
FlushBuffer(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果AI流结束但没有任何分类结果,发出错误提示
|
||||||
|
if (classifyResults.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("AI未返回任何分类结果,buffer最终内容: {BufferPreview}", buffer.ToString().Length > 500 ? buffer.ToString().Substring(0, 500) + "..." : buffer.ToString());
|
||||||
|
chunkAction(("error", "智能分类未返回任何结果,请重试或手动分类"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
chunkAction(("end", "分类完成"));
|
chunkAction(("end", "分类完成"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "智能分类失败");
|
logger.LogError(ex, "智能分类失败");
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
:finished="transactionFinished"
|
:finished="transactionFinished"
|
||||||
@load="loadGroupTransactions"
|
@load="loadGroupTransactions"
|
||||||
@click="handleTransactionClick"
|
@click="handleTransactionClick"
|
||||||
|
@delete="handleGroupTransactionDelete"
|
||||||
/>
|
/>
|
||||||
</PopupContainer>
|
</PopupContainer>
|
||||||
|
|
||||||
@@ -188,7 +189,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
||||||
import {
|
import {
|
||||||
showToast,
|
showToast,
|
||||||
showSuccessToast,
|
showSuccessToast,
|
||||||
@@ -211,7 +212,7 @@ const props = defineProps({
|
|||||||
// 每页数量
|
// 每页数量
|
||||||
pageSize: {
|
pageSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 5
|
default: 3 // TODO 测试写小一点
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -482,6 +483,21 @@ const handleConfirmBatchUpdate = async () => {
|
|||||||
await refresh()
|
await refresh()
|
||||||
// 通知父组件数据已更改
|
// 通知父组件数据已更改
|
||||||
emit('data-changed')
|
emit('data-changed')
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(
|
||||||
|
'transactions-changed',
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
reason: batchGroup.value.reason
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch(e) {
|
||||||
|
console.error('触发全局 transactions-changed 事件失败:', e)
|
||||||
|
}
|
||||||
|
// 关闭弹窗
|
||||||
|
showTransactionList.value = false
|
||||||
} else {
|
} else {
|
||||||
showToast(res.message || '批量更新失败')
|
showToast(res.message || '批量更新失败')
|
||||||
}
|
}
|
||||||
@@ -511,6 +527,67 @@ const handleTransactionClick = (transaction) => {
|
|||||||
showTransactionDetail.value = true
|
showTransactionDetail.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理分组中的删除事件
|
||||||
|
const handleGroupTransactionDelete = async (transactionId) => {
|
||||||
|
groupTransactions.value = groupTransactions.value.filter(t => t.id !== transactionId)
|
||||||
|
groupTransactionsTotal.value = Math.max(0, (groupTransactionsTotal.value || 0) - 1)
|
||||||
|
|
||||||
|
if(groupTransactions.value.length === 0 && !transactionFinished.value) {
|
||||||
|
// 如果当前页数据为空且未加载完,则尝试加载下一页
|
||||||
|
await loadGroupTransactions()
|
||||||
|
}
|
||||||
|
|
||||||
|
if(groupTransactions.value.length === 0){
|
||||||
|
// 如果删除后当前分组没有交易了,关闭弹窗
|
||||||
|
showTransactionList.value = false
|
||||||
|
groups.value = groups.value.filter(g => g.reason !== selectedGroup.value.reason)
|
||||||
|
selectedGroup.value = null
|
||||||
|
total.value--
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载当前页统计(如果需要)并通知父组件数据已更改
|
||||||
|
emit('data-changed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局删除事件监听,刷新当前分组交易或分组数据
|
||||||
|
const onGlobalTransactionDeleted = () => {
|
||||||
|
// 如果当前弹窗打开并存在 selectedGroup,则重新加载分组交易
|
||||||
|
if (showTransactionList.value && selectedGroup.value) {
|
||||||
|
// 重新加载从第一页开始
|
||||||
|
groupTransactions.value = []
|
||||||
|
transactionPageIndex.value = 1
|
||||||
|
transactionFinished.value = false
|
||||||
|
loadGroupTransactions()
|
||||||
|
} else {
|
||||||
|
// 否则刷新分组列表
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当有交易新增/修改/批量更新时的刷新监听
|
||||||
|
const onGlobalTransactionsChanged = (e) => {
|
||||||
|
if (showTransactionList.value && selectedGroup.value) {
|
||||||
|
groupTransactions.value = []
|
||||||
|
transactionPageIndex.value = 1
|
||||||
|
transactionFinished.value = false
|
||||||
|
loadGroupTransactions()
|
||||||
|
} else {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
})
|
||||||
|
|
||||||
// 处理账单保存后的回调
|
// 处理账单保存后的回调
|
||||||
const handleTransactionSaved = async () => {
|
const handleTransactionSaved = async () => {
|
||||||
// 通知父组件数据已更改
|
// 通知父组件数据已更改
|
||||||
|
|||||||
@@ -101,6 +101,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { showConfirmDialog, showToast } from 'vant'
|
||||||
|
import { deleteTransaction } from '@/api/transactionRecord'
|
||||||
|
|
||||||
import { defineEmits } from 'vue'
|
import { defineEmits } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -132,6 +136,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['load', 'click', 'delete', 'update:selectedIds'])
|
const emit = defineEmits(['load', 'click', 'delete', 'update:selectedIds'])
|
||||||
|
|
||||||
|
const deletingIds = ref(new Set())
|
||||||
|
|
||||||
const onLoad = () => {
|
const onLoad = () => {
|
||||||
emit('load')
|
emit('load')
|
||||||
}
|
}
|
||||||
@@ -140,8 +146,35 @@ const handleClick = (transaction) => {
|
|||||||
emit('click', transaction)
|
emit('click', transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteClick = (transaction) => {
|
const handleDeleteClick = async (transaction) => {
|
||||||
emit('delete', transaction)
|
try {
|
||||||
|
await showConfirmDialog({
|
||||||
|
title: '提示',
|
||||||
|
message: '确定要删除这条交易记录吗?'
|
||||||
|
})
|
||||||
|
|
||||||
|
deletingIds.value.add(transaction.id)
|
||||||
|
const response = await deleteTransaction(transaction.id)
|
||||||
|
deletingIds.value.delete(transaction.id)
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
showToast('删除成功')
|
||||||
|
emit('delete', transaction.id)
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(new CustomEvent('transaction-deleted', { detail: transaction.id }))
|
||||||
|
} catch (e) {
|
||||||
|
// ignore in non-browser environment
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast(response.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 用户取消确认会抛出 'cancel' 或类似错误
|
||||||
|
if (err !== 'cancel') {
|
||||||
|
console.error('删除出错:', err)
|
||||||
|
showToast('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelected = (id) => {
|
const isSelected = (id) => {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
:finished="true"
|
:finished="true"
|
||||||
:show-delete="true"
|
:show-delete="true"
|
||||||
@click="viewDetail"
|
@click="viewDetail"
|
||||||
|
@delete="handleDateTransactionDelete"
|
||||||
/>
|
/>
|
||||||
</PopupContainer>
|
</PopupContainer>
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, nextTick } from "vue";
|
import { ref, onMounted, nextTick, onBeforeUnmount } from "vue";
|
||||||
import { showToast } from "vant";
|
import { showToast } from "vant";
|
||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
import { getTransactionDetail, getTransactionsByDate } from "@/api/transactionRecord";
|
import { getTransactionDetail, getTransactionsByDate } from "@/api/transactionRecord";
|
||||||
@@ -214,6 +215,15 @@ const onDetailSave = async (saveData) => {
|
|||||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理删除事件:从当前日期交易列表中移除,并刷新当日和当月统计
|
||||||
|
const handleDateTransactionDelete = async (transactionId) => {
|
||||||
|
dateTransactions.value = dateTransactions.value.filter(t => t.id !== transactionId)
|
||||||
|
|
||||||
|
// 刷新当前日期以及当月的统计数据
|
||||||
|
const now = selectedDate.value || new Date();
|
||||||
|
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||||
|
};
|
||||||
|
|
||||||
// 智能分类保存回调
|
// 智能分类保存回调
|
||||||
const onSmartClassifySave = async () => {
|
const onSmartClassifySave = async () => {
|
||||||
// 保存完成后重新加载数据
|
// 保存完成后重新加载数据
|
||||||
@@ -248,6 +258,36 @@ const formatterCalendar = (day) => {
|
|||||||
// 初始加载当前月份数据
|
// 初始加载当前月份数据
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||||
|
|
||||||
|
// 全局删除事件监听,确保日历页面数据一致
|
||||||
|
const onGlobalTransactionDeleted = (e) => {
|
||||||
|
if (selectedDate.value) {
|
||||||
|
fetchDateTransactions(selectedDate.value)
|
||||||
|
}
|
||||||
|
const now = selectedDate.value || new Date()
|
||||||
|
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当有交易被新增/修改/批量更新时刷新
|
||||||
|
const onGlobalTransactionsChanged = (e) => {
|
||||||
|
if (selectedDate.value) {
|
||||||
|
fetchDateTransactions(selectedDate.value)
|
||||||
|
}
|
||||||
|
const now = selectedDate.value || new Date()
|
||||||
|
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const error = ref(false)
|
|||||||
const finished = ref(false)
|
const finished = ref(false)
|
||||||
const hasData = ref(false)
|
const hasData = ref(false)
|
||||||
const unclassifiedCount = ref(0)
|
const unclassifiedCount = ref(0)
|
||||||
|
const _loadedUnclassifiedInitially = ref(false)
|
||||||
|
|
||||||
// 获取未分类账单统计
|
// 获取未分类账单统计
|
||||||
const loadUnclassifiedCount = async () => {
|
const loadUnclassifiedCount = async () => {
|
||||||
@@ -72,6 +73,10 @@ const handleDataLoaded = ({ groups, total, finished: isFinished }) => {
|
|||||||
// 处理数据变更
|
// 处理数据变更
|
||||||
const handleDataChanged = async () => {
|
const handleDataChanged = async () => {
|
||||||
await loadUnclassifiedCount()
|
await loadUnclassifiedCount()
|
||||||
|
// 重新加载数据
|
||||||
|
listLoading.value = true
|
||||||
|
await onLoad()
|
||||||
|
listLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载更多
|
// 加载更多
|
||||||
@@ -84,13 +89,15 @@ const onLoad = async () => {
|
|||||||
try {
|
try {
|
||||||
await groupListRef.value.loadData()
|
await groupListRef.value.loadData()
|
||||||
|
|
||||||
// 首次加载时获取统计信息
|
// 首次加载时获取统计信息(确保统计不会丢失)
|
||||||
if (!hasData.value) {
|
if (!_loadedUnclassifiedInitially.value) {
|
||||||
await loadUnclassifiedCount()
|
await loadUnclassifiedCount()
|
||||||
|
_loadedUnclassifiedInitially.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
error.value = false
|
error.value = false
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('加载分组数据失败:', err)
|
||||||
error.value = true
|
error.value = true
|
||||||
} finally {
|
} finally {
|
||||||
listLoading.value = false
|
listLoading.value = false
|
||||||
@@ -106,6 +113,13 @@ const handleBack = () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 初始加载数据
|
// 初始加载数据
|
||||||
listLoading.value = true
|
listLoading.value = true
|
||||||
|
try {
|
||||||
|
// 先确保统计加载,避免统计消失
|
||||||
|
await loadUnclassifiedCount()
|
||||||
|
_loadedUnclassifiedInitially.value = true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('初始化加载未分类统计失败:', e)
|
||||||
|
}
|
||||||
await onLoad()
|
await onLoad()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant'
|
import { showToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant'
|
||||||
import {
|
import {
|
||||||
@@ -68,6 +68,7 @@ const totalGroups = ref(0)
|
|||||||
const classifying = ref(false)
|
const classifying = ref(false)
|
||||||
const hasChanges = ref(false)
|
const hasChanges = ref(false)
|
||||||
const classifyBuffer = ref('')
|
const classifyBuffer = ref('')
|
||||||
|
const suppressDataChanged = ref(false)
|
||||||
|
|
||||||
// 计算已选中的数量
|
// 计算已选中的数量
|
||||||
const selectedCount = computed(() => {
|
const selectedCount = computed(() => {
|
||||||
@@ -98,6 +99,11 @@ const handleDataLoaded = ({ groups, total }) => {
|
|||||||
|
|
||||||
// 处理数据变更
|
// 处理数据变更
|
||||||
const handleDataChanged = async () => {
|
const handleDataChanged = async () => {
|
||||||
|
if (suppressDataChanged.value) {
|
||||||
|
suppressDataChanged.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await loadUnclassifiedCount()
|
await loadUnclassifiedCount()
|
||||||
hasChanges.value = false
|
hasChanges.value = false
|
||||||
}
|
}
|
||||||
@@ -191,6 +197,7 @@ const startClassify = async () => {
|
|||||||
|
|
||||||
// 处理SSE事件
|
// 处理SSE事件
|
||||||
const handleSSEEvent = (eventType, data, classifyResults) => {
|
const handleSSEEvent = (eventType, data, classifyResults) => {
|
||||||
|
console.log('收到事件:', eventType, data)
|
||||||
if (eventType === 'data') {
|
if (eventType === 'data') {
|
||||||
try {
|
try {
|
||||||
classifyBuffer.value += data
|
classifyBuffer.value += data
|
||||||
@@ -221,27 +228,32 @@ const handleSSEEvent = (eventType, data, classifyResults) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(jsonStr)
|
const result = JSON.parse(jsonStr)
|
||||||
|
|
||||||
if (result.id && groupListRef.value) {
|
if (result.id && groupListRef.value) {
|
||||||
classifyResults.set(result.id, {
|
classifyResults.set(result.id, {
|
||||||
classify: result.classify || '',
|
classify: result.Classify || '',
|
||||||
type: result.type !== undefined ? result.type : null
|
type: result.Type !== undefined ? result.Type : null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新组件内的分组显示状态
|
// 更新组件内的分组显示状态
|
||||||
const groups = groupListRef.value.getList()
|
const groups = groupListRef.value.getList()
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
if (group.transactionIds.includes(result.id)) {
|
if (group.transactionIds.includes(result.id)) {
|
||||||
group.sampleClassify = result.classify || ''
|
group.sampleClassify = result.Classify || ''
|
||||||
if (result.type !== undefined && result.type !== null) {
|
if (result.Type !== undefined && result.Type !== null) {
|
||||||
group.sampleType = result.type
|
group.sampleType = result.Type
|
||||||
}
|
}
|
||||||
hasChanges.value = true
|
hasChanges.value = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 更新回组件
|
// 更新回组件(内部更新时抑制 data-changed 处理)
|
||||||
|
suppressDataChanged.value = true
|
||||||
groupListRef.value.setList(groups)
|
groupListRef.value.setList(groups)
|
||||||
|
// 确保在子组件内部事件触发后恢复标志并保持 hasChanges
|
||||||
|
nextTick(() => {
|
||||||
|
suppressDataChanged.value = false
|
||||||
|
hasChanges.value = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('JSON解析失败:', e)
|
console.error('JSON解析失败:', e)
|
||||||
|
|||||||
@@ -129,6 +129,7 @@
|
|||||||
:finished="true"
|
:finished="true"
|
||||||
:show-delete="true"
|
:show-delete="true"
|
||||||
@click="handleTransactionClick"
|
@click="handleTransactionClick"
|
||||||
|
@delete="handleTransactionDelete"
|
||||||
/>
|
/>
|
||||||
</PopupContainer>
|
</PopupContainer>
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { showToast, showConfirmDialog } from 'vant'
|
import { showToast, showConfirmDialog } from 'vant'
|
||||||
import { getEmailList, getEmailDetail, deleteEmail, refreshTransactionRecords, syncEmails, getEmailTransactions } from '@/api/emailRecord'
|
import { getEmailList, getEmailDetail, deleteEmail, refreshTransactionRecords, syncEmails, getEmailTransactions } from '@/api/emailRecord'
|
||||||
import { getTransactionDetail } from '@/api/transactionRecord'
|
import { getTransactionDetail } from '@/api/transactionRecord'
|
||||||
@@ -281,7 +282,7 @@ const handleRefreshAnalysis = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
refreshingAnalysis.value = true
|
refreshingAnalysis.value = true
|
||||||
const response = await refreshTransactionRecords(currentEmail.value.id || currentEmail.value.Id)
|
const response = await refreshTransactionRecords(currentEmail.value.id)
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
showToast('重新分析成功')
|
showToast('重新分析成功')
|
||||||
@@ -325,7 +326,7 @@ const viewTransactions = async () => {
|
|||||||
if (!currentEmail.value) return
|
if (!currentEmail.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const emailId = currentEmail.value.id || currentEmail.value.Id
|
const emailId = currentEmail.value.id
|
||||||
const response = await getEmailTransactions(emailId)
|
const response = await getEmailTransactions(emailId)
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -340,6 +341,46 @@ const viewTransactions = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听全局删除事件,保持弹窗内交易列表一致
|
||||||
|
const onGlobalTransactionDeleted = (e) => {
|
||||||
|
// 如果交易列表弹窗打开,尝试重新加载邮箱的交易列表
|
||||||
|
if (transactionListVisible.value && currentEmail.value) {
|
||||||
|
const emailId = currentEmail.value.id || currentEmail.value.Id
|
||||||
|
getEmailTransactions(emailId).then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
transactionList.value = response.data || []
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听新增/修改/批量更新事件,刷新弹窗内交易或邮件列表
|
||||||
|
const onGlobalTransactionsChanged = (e) => {
|
||||||
|
if (transactionListVisible.value && currentEmail.value) {
|
||||||
|
const emailId = currentEmail.value.id || currentEmail.value.Id
|
||||||
|
getEmailTransactions(emailId).then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
transactionList.value = response.data || []
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
} else {
|
||||||
|
// 也刷新邮件列表以保持统计一致
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
})
|
||||||
|
|
||||||
// 处理点击账单
|
// 处理点击账单
|
||||||
const handleTransactionClick = async (transaction) => {
|
const handleTransactionClick = async (transaction) => {
|
||||||
try {
|
try {
|
||||||
@@ -356,16 +397,36 @@ const handleTransactionClick = async (transaction) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTransactionDelete = (transactionId) => {
|
||||||
|
// 从当前的交易列表中移除该交易
|
||||||
|
transactionList.value = transactionList.value.filter(t => t.id !== transactionId)
|
||||||
|
|
||||||
|
// 刷新邮件列表
|
||||||
|
loadData(true)
|
||||||
|
|
||||||
|
// 刷新当前邮件详情
|
||||||
|
if (currentEmail.value) {
|
||||||
|
const emailId = currentEmail.value.id
|
||||||
|
getEmailDetail(emailId).then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
currentEmail.value = response.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try { window.dispatchEvent(new CustomEvent('transaction-deleted', { detail: transactionId })) } catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// 账单保存后刷新列表
|
// 账单保存后刷新列表
|
||||||
const handleTransactionSave = async () => {
|
const handleTransactionSave = async () => {
|
||||||
// 刷新账单列表
|
// 刷新账单列表
|
||||||
if (currentEmail.value) {
|
if (currentEmail.value) {
|
||||||
const emailId = currentEmail.value.id || currentEmail.value.Id
|
const emailId = currentEmail.value.id
|
||||||
const response = await getEmailTransactions(emailId)
|
const response = await getEmailTransactions(emailId)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
transactionList.value = response.data || []
|
transactionList.value = response.data || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try { window.dispatchEvent(new CustomEvent('transactions-changed', { detail: { emailId: currentEmail.value?.id } })) } catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
:show-delete="true"
|
:show-delete="true"
|
||||||
@load="loadCategoryBills"
|
@load="loadCategoryBills"
|
||||||
@click="viewBillDetail"
|
@click="viewBillDetail"
|
||||||
|
@delete="handleCategoryBillsDelete"
|
||||||
/>
|
/>
|
||||||
</PopupContainer>
|
</PopupContainer>
|
||||||
|
|
||||||
@@ -315,6 +316,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onActivated, nextTick } from 'vue'
|
import { ref, computed, onMounted, onActivated, nextTick } from 'vue'
|
||||||
|
import { onBeforeUnmount } from 'vue'
|
||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { getMonthlyStatistics, getCategoryStatistics, getTrendStatistics } from '@/api/statistics'
|
import { getMonthlyStatistics, getCategoryStatistics, getTrendStatistics } from '@/api/statistics'
|
||||||
@@ -715,6 +717,14 @@ const viewBillDetail = async (transaction) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCategoryBillsDelete = (deletedId) => {
|
||||||
|
categoryBills.value = categoryBills.value.filter(t => t.id !== deletedId)
|
||||||
|
categoryBillsTotal.value--
|
||||||
|
|
||||||
|
// 被删除后刷新统计数据和账单列表
|
||||||
|
fetchStatistics()
|
||||||
|
}
|
||||||
|
|
||||||
// 账单保存后的回调
|
// 账单保存后的回调
|
||||||
const onBillSave = async () => {
|
const onBillSave = async () => {
|
||||||
// 刷新统计数据
|
// 刷新统计数据
|
||||||
@@ -746,6 +756,7 @@ const onSmartClassifySave = async () => {
|
|||||||
|
|
||||||
// 刷新统计数据
|
// 刷新统计数据
|
||||||
await fetchStatistics()
|
await fetchStatistics()
|
||||||
|
try { window.dispatchEvent(new CustomEvent('transactions-changed', { detail: { reason: selectedClassify.value, type: selectedType.value } })) } catch(e) {}
|
||||||
|
|
||||||
showToast('智能分类已保存')
|
showToast('智能分类已保存')
|
||||||
}
|
}
|
||||||
@@ -778,6 +789,28 @@ onMounted(() => {
|
|||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
fetchStatistics()
|
fetchStatistics()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 全局监听交易删除事件,确保统计数据一致
|
||||||
|
const onGlobalTransactionDeleted = (e) => {
|
||||||
|
// e.detail contains transaction id
|
||||||
|
fetchStatistics()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onGlobalTransactionsChanged = (e) => {
|
||||||
|
fetchStatistics()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -12,9 +12,13 @@
|
|||||||
:transactions="transactionList"
|
:transactions="transactionList"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:finished="finished"
|
:finished="finished"
|
||||||
|
:show-delete="true"
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
@click="viewDetail"
|
@click="viewDetail"
|
||||||
@delete="handleDelete"
|
@delete="(id) => {
|
||||||
|
// 从当前的交易列表中移除该交易
|
||||||
|
transactionList.value = transactionList.value.filter(t => t.id !== id)
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 底部安全距离 -->
|
<!-- 底部安全距离 -->
|
||||||
@@ -161,8 +165,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { showToast, showConfirmDialog } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import {
|
import {
|
||||||
getTransactionList,
|
getTransactionList,
|
||||||
getTransactionDetail,
|
getTransactionDetail,
|
||||||
@@ -291,7 +295,7 @@ const loadData = async (isRefresh = false) => {
|
|||||||
const onRefresh = () => {
|
const onRefresh = () => {
|
||||||
finished.value = false
|
finished.value = false
|
||||||
transactionList.value = []
|
transactionList.value = []
|
||||||
loadData(false)
|
loadData(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索相关方法
|
// 搜索相关方法
|
||||||
@@ -345,28 +349,7 @@ const onDetailSave = async () => {
|
|||||||
await loadClassifyList()
|
await loadClassifyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除
|
// 删除功能由 TransactionList 组件内部处理,组件通过 :show-delete 启用
|
||||||
const handleDelete = async (transaction) => {
|
|
||||||
try {
|
|
||||||
await showConfirmDialog({
|
|
||||||
title: '提示',
|
|
||||||
message: '确定要删除这条交易记录吗?',
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await deleteTransaction(transaction.id)
|
|
||||||
if (response.success) {
|
|
||||||
showToast('删除成功')
|
|
||||||
loadData(true)
|
|
||||||
} else {
|
|
||||||
showToast(response.message || '删除失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error !== 'cancel') {
|
|
||||||
console.error('删除出错:', error)
|
|
||||||
showToast('删除失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开新增弹窗
|
// 打开新增弹窗
|
||||||
const openAddDialog = () => {
|
const openAddDialog = () => {
|
||||||
@@ -484,6 +467,7 @@ const onAddSubmit = async () => {
|
|||||||
showToast('添加成功')
|
showToast('添加成功')
|
||||||
addDialogVisible.value = false
|
addDialogVisible.value = false
|
||||||
loadData(true)
|
loadData(true)
|
||||||
|
try { window.dispatchEvent(new CustomEvent('transactions-changed', { detail: response.data })) } catch(e) {}
|
||||||
// 重新加载分类列表
|
// 重新加载分类列表
|
||||||
await loadClassifyList()
|
await loadClassifyList()
|
||||||
} else {
|
} else {
|
||||||
@@ -502,6 +486,35 @@ onMounted(async () => {
|
|||||||
// 不需要手动调用 loadData,van-list 会自动触发 onLoad
|
// 不需要手动调用 loadData,van-list 会自动触发 onLoad
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听全局删除事件,保持页面一致性
|
||||||
|
const onGlobalTransactionDeleted = (e) => {
|
||||||
|
// 如果在此页面,重新刷新当前列表以保持数据一致
|
||||||
|
transactionList.value = []
|
||||||
|
pageIndex.value = 1
|
||||||
|
finished.value = false
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 外部新增/修改/批量更新时的刷新监听
|
||||||
|
const onGlobalTransactionsChanged = (e) => {
|
||||||
|
transactionList.value = []
|
||||||
|
pageIndex.value = 1
|
||||||
|
finished.value = false
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener && window.addEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener && window.removeEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||||
|
})
|
||||||
|
|
||||||
// 暴露给父级方法调用
|
// 暴露给父级方法调用
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openAddDialog
|
openAddDialog
|
||||||
|
|||||||
@@ -120,11 +120,10 @@ public class LogController(ILogger<LogController> logger) : ControllerBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 合并多行日志(已废弃,现在在流式读取中处理)
|
/// 合并多行日志(已废弃,现在在流式读取中处理)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use ReadLogsStreamAsync instead")]
|
|
||||||
private List<string> MergeMultiLineLog(string[] lines)
|
private List<string> MergeMultiLineLog(string[] lines)
|
||||||
{
|
{
|
||||||
var mergedLines = new List<string>();
|
var mergedLines = new List<string>();
|
||||||
var currentLog = new System.Text.StringBuilder();
|
var currentLog = new StringBuilder();
|
||||||
|
|
||||||
// 日志行开始的正则表达式
|
// 日志行开始的正则表达式
|
||||||
var logStartPattern = new System.Text.RegularExpressions.Regex(
|
var logStartPattern = new System.Text.RegularExpressions.Regex(
|
||||||
@@ -175,9 +174,11 @@ public class LogController(ILogger<LogController> logger) : ControllerBase
|
|||||||
{
|
{
|
||||||
// 日志格式示例: [2025-12-29 16:38:45.730 +08:00] [INF] Message here
|
// 日志格式示例: [2025-12-29 16:38:45.730 +08:00] [INF] Message here
|
||||||
// 使用正则表达式解析
|
// 使用正则表达式解析
|
||||||
|
// 使用 Singleline 模式使 '.' 可以匹配换行,这样 multi-line 消息可以被完整捕获。
|
||||||
var match = System.Text.RegularExpressions.Regex.Match(
|
var match = System.Text.RegularExpressions.Regex.Match(
|
||||||
line,
|
line,
|
||||||
@"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2})\] \[([A-Z]{3})\] (.*)$"
|
@"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2})\] \[([A-Z]{2,5})\] ([\s\S]*)$",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.Singleline
|
||||||
);
|
);
|
||||||
|
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
@@ -214,82 +215,30 @@ public class LogController(ILogger<LogController> logger) : ControllerBase
|
|||||||
string? searchKeyword,
|
string? searchKeyword,
|
||||||
string? logLevel)
|
string? logLevel)
|
||||||
{
|
{
|
||||||
var filteredEntries = new List<LogEntry>();
|
// 简化:一次性读取所有行,合并多行日志,过滤并在内存中分页
|
||||||
var currentLog = new System.Text.StringBuilder();
|
var allLines = await ReadAllLinesAsync(path);
|
||||||
var logStartPattern = new System.Text.RegularExpressions.Regex(
|
|
||||||
@"^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2}\]");
|
|
||||||
|
|
||||||
// 计算需要读取的最大条目数(取最近的N条日志用于倒序分页)
|
// 合并多行日志为独立条目
|
||||||
// 由于日志倒序显示,我们读取足够的数据以覆盖当前页
|
var merged = MergeMultiLineLog(allLines);
|
||||||
var maxEntriesToRead = pageIndex * pageSize + pageSize; // 多读一页用于判断是否有下一页
|
|
||||||
|
|
||||||
using var fileStream = new FileStream(
|
var parsed = new List<LogEntry>();
|
||||||
path,
|
foreach (var line in merged)
|
||||||
FileMode.Open,
|
|
||||||
FileAccess.Read,
|
|
||||||
FileShare.ReadWrite);
|
|
||||||
using var streamReader = new StreamReader(fileStream);
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
var readCount = 0;
|
|
||||||
|
|
||||||
while ((line = await streamReader.ReadLineAsync()) != null)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
var entry = ParseLogLine(line);
|
||||||
continue;
|
if (entry != null && PassFilter(entry, searchKeyword, logLevel))
|
||||||
|
|
||||||
// 检查是否是新的日志条目
|
|
||||||
if (logStartPattern.IsMatch(line))
|
|
||||||
{
|
{
|
||||||
// 处理之前累积的日志
|
parsed.Add(entry);
|
||||||
if (currentLog.Length > 0)
|
|
||||||
{
|
|
||||||
var logEntry = ParseLogLine(currentLog.ToString());
|
|
||||||
if (logEntry != null && PassFilter(logEntry, searchKeyword, logLevel))
|
|
||||||
{
|
|
||||||
filteredEntries.Add(logEntry);
|
|
||||||
readCount++;
|
|
||||||
|
|
||||||
// 如果已读取足够数据,提前退出
|
|
||||||
if (readCount >= maxEntriesToRead)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentLog.Clear();
|
|
||||||
}
|
|
||||||
currentLog.Append(line);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 这是上一条日志的延续
|
|
||||||
if (currentLog.Length > 0)
|
|
||||||
{
|
|
||||||
currentLog.Append('\n').Append(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理最后一条日志(如果循环正常结束或刚好在日志边界退出)
|
|
||||||
if (currentLog.Length > 0 && readCount < maxEntriesToRead)
|
|
||||||
{
|
|
||||||
var logEntry = ParseLogLine(currentLog.ToString());
|
|
||||||
if (logEntry != null && PassFilter(logEntry, searchKeyword, logLevel))
|
|
||||||
{
|
|
||||||
filteredEntries.Add(logEntry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 倒序排列(最新的在前面)
|
// 倒序(最新在前)
|
||||||
filteredEntries.Reverse();
|
parsed.Reverse();
|
||||||
|
|
||||||
// 计算分页
|
var total = parsed.Count;
|
||||||
var skip = (pageIndex - 1) * pageSize;
|
var skip = Math.Max(0, (pageIndex - 1) * pageSize);
|
||||||
var pagedData = filteredEntries.Skip(skip).Take(pageSize).ToList();
|
var pagedData = parsed.Skip(skip).Take(pageSize).ToList();
|
||||||
|
|
||||||
// total 返回 -1 表示未知(避免扫描整个文件)
|
return (pagedData, total);
|
||||||
// 前端可以根据返回数据量判断是否有下一页
|
|
||||||
return (pagedData, -1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ public static class Expand
|
|||||||
q.AddTrigger(opts => opts
|
q.AddTrigger(opts => opts
|
||||||
.ForJob(emailJobKey)
|
.ForJob(emailJobKey)
|
||||||
.WithIdentity("EmailSyncTrigger")
|
.WithIdentity("EmailSyncTrigger")
|
||||||
.WithCronSchedule("0 0/10 * * * ?") // 每10分钟执行
|
.WithCronSchedule("0 0/30 * * * ?") // 每30分钟执行
|
||||||
.WithDescription("每10分钟同步一次邮件"));
|
.WithDescription("每30分钟同步一次邮件"));
|
||||||
|
|
||||||
// 配置周期性账单任务 - 每天早上6点执行
|
// 配置周期性账单任务 - 每天早上6点执行
|
||||||
var periodicBillJobKey = new JobKey("PeriodicBillJob");
|
var periodicBillJobKey = new JobKey("PeriodicBillJob");
|
||||||
|
|||||||
Reference in New Issue
Block a user