refactor: 前端 SSE 鉴权改造与公共逻辑抽取,修复统计分组空指针

- SSE 由 EventSource 改为 fetch 流式读取(新增 utils/sse.ts),token 走 Authorization 请求头,避免令牌经 URL 泄露到访问日志
- 抽取 useKeywordHistory,统一备注关键词与 AI 查询历史的读写、下拉与清空
- 抽取 renderCategoryLabel 至 utils/render.ts,消除多处重复实现
- 导出改用统一 request 实例(自动携带鉴权头),并将 blob 错误体解析为业务异常
- 新增 untagged 筛选参数;AI 智能查询改为弹窗入口并补充三语文案
- 分类/标签/邮箱/交易表单补充提交 loading,避免重复提交
- 交易时间改用本地格式化,修复 toISOString 造成的时区偏移
- 移除 Naive UI 全局注册;修正 NLog 的 rows 属性为 lines
- 修复未分类记录分组时 categoryMap.get(null) 抛出的 NPE
This commit is contained in:
2026-09-20 15:09:35 +08:00
parent 5fb3d78ce0
commit 0a8811534b
17 changed files with 476 additions and 375 deletions
+19 -15
View File
@@ -10,6 +10,7 @@ import type { DataTableColumns } from 'naive-ui'
import type { ParsedBillRow, BillImportItem } from '../api/transaction'
import { getCategoryList } from '../api/category'
import { getTagList } from '../api/tag'
import { renderCategoryLabel } from '../utils/render'
const props = defineProps<{
/** 解析结果行 */
@@ -34,6 +35,14 @@ const { t } = useI18n()
// 勾选状态
const checkedRowNos = ref<number[]>([])
// 用户对「计入统计」的临时覆盖(按行号),避免直接修改父组件传入的 props.rows
const includeOverrides = ref<Record<number, boolean>>({})
/** 读取某行是否计入统计(优先取用户覆盖值) */
function rowIncludeInStats(row: ParsedBillRow): boolean {
return includeOverrides.value[row.rowNo] ?? row.includeInStats
}
// 筛选条件
const filterKeyword = ref('')
const filterType = ref<string | null>(null)
@@ -83,7 +92,7 @@ const filteredRows = computed(() => {
if (!text.includes(kw)) return false
}
if (filterType.value === 'NEUTRAL') {
if (r.includeInStats) return false
if (rowIncludeInStats(r)) return false
} else if (filterType.value && r.type !== filterType.value) {
return false
}
@@ -143,7 +152,7 @@ const columns = computed<DataTableColumns<ParsedBillRow>>(() => [
if (!row.importable) {
return h(NTag, { type: 'error', size: 'small' }, { default: () => t('import.notImportable') })
}
if (!row.includeInStats) {
if (!rowIncludeInStats(row)) {
return h(NTag, { type: 'default', size: 'small' }, { default: () => t('import.notIncluded') })
}
return h(
@@ -160,7 +169,7 @@ const columns = computed<DataTableColumns<ParsedBillRow>>(() => [
align: 'center',
render: (row) =>
h(NCheckbox, {
checked: !!row.includeInStats,
checked: rowIncludeInStats(row),
disabled: !row.importable,
onUpdateChecked: (v: boolean) => toggleIncludeInStats(row, v),
}),
@@ -236,14 +245,6 @@ async function loadCategories() {
}
}
/** 分类下拉项渲染:图标 + 名称 */
function renderCategoryLabel(option: any) {
return h('span', null, [
option.icon ? h('span', { style: { marginRight: '6px' } }, option.icon) : null,
option.label,
])
}
/** 加载标签选项 */
async function loadTags() {
try {
@@ -272,7 +273,7 @@ function toggleAll(checked: boolean) {
/** 切换单行是否计入统计 */
function toggleIncludeInStats(row: ParsedBillRow, value: boolean) {
row.includeInStats = value
includeOverrides.value = { ...includeOverrides.value, [row.rowNo]: value }
}
/** 批量设置已勾选行的计入统计标记 */
@@ -281,9 +282,11 @@ function setCheckedIncludeInStats(value: boolean) {
message.warning(t('import.selectAtLeastOne'))
return
}
const next = { ...includeOverrides.value }
checkedRows.value.forEach((row) => {
row.includeInStats = value
next[row.rowNo] = value
})
includeOverrides.value = next
}
/** 确认导入:向父组件抛出勾选行与默认归类 */
@@ -299,7 +302,7 @@ function handleConfirm() {
amount: r.amount,
counterparty: r.counterparty,
product: r.product,
includeInStats: r.includeInStats,
includeInStats: rowIncludeInStats(r),
rawData: r.rawData,
}))
emit('confirm', {
@@ -310,9 +313,10 @@ function handleConfirm() {
})
}
// 解析结果变化时重置勾选(默认选中解析器建议行)与筛选条件
// 解析结果变化时重置勾选(默认选中解析器建议行)、计入统计覆盖与筛选条件
watch(() => props.rows, (rows) => {
checkedRowNos.value = rows.filter((r) => r.selected).map((r) => r.rowNo)
includeOverrides.value = {}
resetFilters()
}, { immediate: true })