feat: 支持数据库运行时配置与多数据库,重构设置页并完善打包

- 数据库支持 SQLite / MySQL 运行时配置与切换,未配置时仍可启动,配置后自动建表
- 新增默认分类与标签初始化,注册用户自动创建
- 新增注册开关(默认关闭,可在系统设置开启;无用户时允许创建首个账号)
- 合并数据库、注册与 AI 配置为「系统设置」页,拆分「模型使用情况」独立页面
- AI 一键设置支持新建分类与标签
- 前端构建产物打包进可执行 jar,后端增加 SPA 路由回退
- jar 更名为 finance-app,补全 Gradle Wrapper,移除 archive 任务
- 更新 DeepSeek 模型说明文案
This commit is contained in:
2026-09-18 12:33:41 +08:00
parent 16e4c30456
commit c1f5d02564
63 changed files with 3783 additions and 573 deletions
+95 -4
View File
@@ -1007,17 +1007,47 @@ async function handleAiApply() {
message.warning(t('ai.applyNone'))
return
}
// 勾选中包含新建分类/标签时,先经用户确认再落库
if (aiNewCategoryNames.value.length > 0 || aiNewTagNames.value.length > 0) {
const confirmed = await new Promise<boolean>((resolve) => {
dialog.warning({
title: t('ai.applyNewTitle'),
content: t('ai.applyNewConfirm', {
categories: aiNewCategoryNames.value.length,
tags: aiNewTagNames.value.length,
}),
positiveText: t('common.confirm'),
negativeText: t('common.cancel'),
onPositiveClick: () => resolve(true),
onNegativeClick: () => resolve(false),
onClose: () => resolve(false),
})
})
if (!confirmed) {
return
}
}
aiApplying.value = true
try {
// 仅提交勾选的建议,聚合建议按组展开为逐条记录
const payload = chosen.flatMap((s) => {
const ids = s.transactionIds?.length ? s.transactionIds : [s.transactionId]
return ids.map((id) => ({ transactionId: id, categoryId: s.categoryId, tagIds: s.tagIds || [] }))
return ids.map((id) => ({
transactionId: id,
categoryId: s.categoryId,
newCategoryName: s.newCategoryName,
newCategoryIcon: s.newCategoryIcon,
tagIds: s.tagIds || [],
newTags: s.newTags || [],
}))
})
const res: any = await applyAiSuggestions(payload)
message.success(t('ai.applySuccess', { count: res.data }))
showAiModal.value = false
await fetchData()
// 应用过程中可能新建了分类或标签,刷新以保持一致
await fetchCategories()
await fetchTags()
} catch (e: any) {
message.error(e.message || t('ai.applyFailed'))
} finally {
@@ -1344,6 +1374,30 @@ const aiTotalCount = computed(() => {
.reduce((sum, s) => sum + (s.transactionIds?.length || 1), 0)
})
/** 勾选建议中需要新建的分类名称(去重) */
const aiNewCategoryNames = computed(() => {
const selected = new Set(checkedAiRowKeys.value)
const names = new Set<string>()
aiSuggestions.value.forEach((s) => {
if (selected.has(s.transactionId) && s.newCategoryName) {
names.add(s.newCategoryName)
}
})
return Array.from(names)
})
/** 勾选建议中需要新建的标签名称(去重) */
const aiNewTagNames = computed(() => {
const selected = new Set(checkedAiRowKeys.value)
const names = new Set<string>()
aiSuggestions.value.forEach((s) => {
if (selected.has(s.transactionId) && s.newTags) {
s.newTags.forEach((tag) => names.add(tag.name))
}
})
return Array.from(names)
})
/** AI 建议表格列定义(随语言切换与建议范围响应式更新) */
const aiColumns = computed<DataTableColumns<any>>(() => {
const columns: DataTableColumns<any> = [
@@ -1365,18 +1419,55 @@ const aiColumns = computed<DataTableColumns<any>>(() => {
]
// 按建议范围展示对应列
if (aiScope.value !== 'TAG') {
columns.push({ title: t('ai.column.category'), key: 'category', width: 130, render: (row) => row.categoryName || '-' })
columns.push({
title: t('ai.column.category'), key: 'category', minWidth: 150,
render: (row) => renderCategoryCell(row),
})
}
if (aiScope.value !== 'CATEGORY') {
columns.push({
title: t('ai.column.tags'), key: 'tags', minWidth: 150,
render: (row) => (row.tagNames && row.tagNames.length > 0 ? row.tagNames.join('、') : '-'),
title: t('ai.column.tags'), key: 'tags', minWidth: 180,
render: (row) => renderTagsCell(row),
})
}
columns.push({ title: t('ai.column.reason'), key: 'reason', minWidth: 180, ellipsis: { tooltip: true } })
return columns
})
/** 渲染 AI 建议的分类单元格,新建分类带「新建」标记 */
function renderCategoryCell(row: AiSuggestion) {
if (row.newCategoryName) {
return h(NSpace, { size: 4, align: 'center', wrap: false }, {
default: () => [
h(NTag, { size: 'small', type: 'info' }, { default: () => t('ai.newBadge') }),
h('span', null, `${row.newCategoryIcon || ''}${row.newCategoryName}`),
],
})
}
return row.categoryName || '-'
}
/** 渲染 AI 建议的标签单元格,新建标签带「新建」标记并使用建议颜色 */
function renderTagsCell(row: AiSuggestion) {
const items: any[] = []
if (row.tagNames && row.tagNames.length > 0) {
items.push(...row.tagNames.map((name) => h(NTag, { size: 'small' }, { default: () => name })))
}
if (row.newTags && row.newTags.length > 0) {
items.push(...row.newTags.map((tag) =>
h(NTag, { size: 'small', color: { color: tag.color, textColor: '#fff' } }, {
default: () => h(NSpace, { size: 2, align: 'center', wrap: false }, {
default: () => [
h('span', null, t('ai.newBadge')),
h('span', null, tag.name),
],
}),
}),
))
}
return items.length > 0 ? h(NSpace, { size: 4, wrap: true }, { default: () => items }) : '-'
}
/** 打开批量设置分类弹窗 */
function handleBatchOpen() {
if (transactions.value.length === 0) {