feat: 支持数据库运行时配置与多数据库,重构设置页并完善打包
- 数据库支持 SQLite / MySQL 运行时配置与切换,未配置时仍可启动,配置后自动建表 - 新增默认分类与标签初始化,注册用户自动创建 - 新增注册开关(默认关闭,可在系统设置开启;无用户时允许创建首个账号) - 合并数据库、注册与 AI 配置为「系统设置」页,拆分「模型使用情况」独立页面 - AI 一键设置支持新建分类与标签 - 前端构建产物打包进可执行 jar,后端增加 SPA 路由回退 - jar 更名为 finance-app,补全 Gradle Wrapper,移除 archive 任务 - 更新 DeepSeek 模型说明文案
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user