- 数据库支持 SQLite / MySQL 运行时配置与切换,未配置时仍可启动,配置后自动建表 - 新增默认分类与标签初始化,注册用户自动创建 - 新增注册开关(默认关闭,可在系统设置开启;无用户时允许创建首个账号) - 合并数据库、注册与 AI 配置为「系统设置」页,拆分「模型使用情况」独立页面 - AI 一键设置支持新建分类与标签 - 前端构建产物打包进可执行 jar,后端增加 SPA 路由回退 - jar 更名为 finance-app,补全 Gradle Wrapper,移除 archive 任务 - 更新 DeepSeek 模型说明文案
238 lines
7.6 KiB
Vue
238 lines
7.6 KiB
Vue
<script setup lang="ts">
|
|
// 数据库配置面板:可复用于「系统设置」页与首次配置引导页。
|
|
// setup 为 true 时表示首次引导,保存成功后跳转登录页。
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import {
|
|
useMessage, NCard, NForm, NFormItem, NInput, NInputNumber, NButton, NSpace,
|
|
NAlert, NTag, NRadioGroup, NRadioButton, NIcon, NText, NDescriptions, NDescriptionsItem,
|
|
} from 'naive-ui'
|
|
import { ServerOutline, SaveOutline, FlashOutline, FolderOpenOutline } from '@vicons/ionicons5'
|
|
import { getDatabaseStatus, testDatabaseConnection, saveDatabaseConfig } from '../api/database'
|
|
import type { DatabaseConfigPayload, DatabaseType } from '../api/database'
|
|
import { useDatabaseStore } from '../store/database'
|
|
|
|
const props = defineProps<{ setup?: boolean }>()
|
|
|
|
const router = useRouter()
|
|
const message = useMessage()
|
|
const { t } = useI18n()
|
|
const databaseStore = useDatabaseStore()
|
|
|
|
const testing = ref(false)
|
|
const saving = ref(false)
|
|
const configured = ref(false)
|
|
const hasPassword = ref(false)
|
|
|
|
// 表单数据
|
|
const form = ref({
|
|
type: 'SQLITE' as DatabaseType,
|
|
sqlitePath: 'data/finance.db',
|
|
host: 'localhost',
|
|
port: 3306,
|
|
database: '',
|
|
username: '',
|
|
password: '',
|
|
})
|
|
|
|
/** 加载当前数据库状态并回显到表单 */
|
|
async function loadStatus() {
|
|
try {
|
|
const res: any = await getDatabaseStatus()
|
|
const data = res.data
|
|
configured.value = !!data?.configured
|
|
if (data?.configured) {
|
|
form.value.type = data.type
|
|
if (data.type === 'SQLITE') {
|
|
form.value.sqlitePath = data.sqlitePath || 'data/finance.db'
|
|
} else {
|
|
form.value.host = data.host || 'localhost'
|
|
form.value.port = data.port || 3306
|
|
form.value.database = data.database || ''
|
|
form.value.username = data.username || ''
|
|
}
|
|
hasPassword.value = data.type === 'MYSQL'
|
|
}
|
|
form.value.password = ''
|
|
} catch (e: any) {
|
|
message.error(e.message || t('database.loadFailed'))
|
|
}
|
|
}
|
|
|
|
/** 构建提交给后端的配置对象 */
|
|
function buildPayload(): DatabaseConfigPayload {
|
|
if (form.value.type === 'SQLITE') {
|
|
return { type: 'SQLITE', sqlitePath: form.value.sqlitePath }
|
|
}
|
|
return {
|
|
type: 'MYSQL',
|
|
host: form.value.host,
|
|
port: form.value.port,
|
|
database: form.value.database,
|
|
username: form.value.username,
|
|
// 留空表示沿用已保存的密码
|
|
password: form.value.password || undefined,
|
|
}
|
|
}
|
|
|
|
/** 前端必填校验 */
|
|
function validate(): boolean {
|
|
if (form.value.type === 'SQLITE') {
|
|
if (!form.value.sqlitePath?.trim()) {
|
|
message.warning(t('database.sqlitePathRequired'))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
if (!form.value.host?.trim() || !form.value.database?.trim() || !form.value.username?.trim()) {
|
|
message.warning(t('database.mysqlRequired'))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
/** 测试数据库连接 */
|
|
async function handleTest() {
|
|
if (!validate()) return
|
|
testing.value = true
|
|
try {
|
|
await testDatabaseConnection(buildPayload())
|
|
message.success(t('database.testSuccess'))
|
|
} catch (e: any) {
|
|
message.error(e.message || t('database.testFailed'))
|
|
} finally {
|
|
testing.value = false
|
|
}
|
|
}
|
|
|
|
/** 保存配置并切换到新数据库 */
|
|
async function handleSave() {
|
|
if (!validate()) return
|
|
saving.value = true
|
|
try {
|
|
const res: any = await saveDatabaseConfig(buildPayload())
|
|
databaseStore.setStatus(res.data)
|
|
message.success(t('database.saveSuccess'))
|
|
if (props.setup) {
|
|
// 首次配置完成后引导用户登录
|
|
router.push('/login')
|
|
} else {
|
|
await loadStatus()
|
|
}
|
|
} catch (e: any) {
|
|
message.error(e.message || t('database.saveFailed'))
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(loadStatus)
|
|
</script>
|
|
|
|
<template>
|
|
<NCard :bordered="false" class="config-card">
|
|
<template #header>
|
|
<NSpace align="center" :size="8">
|
|
<NIcon :component="ServerOutline" />
|
|
<span>{{ props.setup ? t('database.setupTitle') : t('settings.databaseSection') }}</span>
|
|
</NSpace>
|
|
</template>
|
|
<template #header-extra>
|
|
<NTag :type="configured ? 'success' : 'warning'" size="small" :bordered="false">
|
|
{{ configured ? t('database.configured') : t('database.notConfigured') }}
|
|
</NTag>
|
|
</template>
|
|
|
|
<NAlert type="info" :show-icon="true" style="margin-bottom: 20px;">
|
|
{{ props.setup ? t('database.setupIntro') : t('database.intro') }}
|
|
</NAlert>
|
|
|
|
<!-- 当前配置概览 -->
|
|
<NDescriptions v-if="configured" :column="1" label-placement="left" size="small" class="db-current">
|
|
<NDescriptionsItem :label="t('database.currentType')">
|
|
<NTag size="small" :bordered="false">{{ databaseStore.status?.typeName }}</NTag>
|
|
</NDescriptionsItem>
|
|
<NDescriptionsItem :label="t('database.currentSummary')">
|
|
<NText code>{{ databaseStore.status?.summary }}</NText>
|
|
</NDescriptionsItem>
|
|
<NDescriptionsItem :label="t('database.configFile')">
|
|
<NText depth="3" style="font-size: 12px;">{{ databaseStore.status?.configFile }}</NText>
|
|
</NDescriptionsItem>
|
|
</NDescriptions>
|
|
|
|
<NForm label-placement="left" label-width="120">
|
|
<NFormItem :label="t('database.type')">
|
|
<NRadioGroup v-model:value="form.type">
|
|
<NRadioButton value="SQLITE">SQLite</NRadioButton>
|
|
<NRadioButton value="MYSQL">MySQL</NRadioButton>
|
|
</NRadioGroup>
|
|
</NFormItem>
|
|
|
|
<!-- SQLite 配置 -->
|
|
<template v-if="form.type === 'SQLITE'">
|
|
<NFormItem :label="t('database.sqlitePath')">
|
|
<NInput v-model:value="form.sqlitePath" :placeholder="t('database.sqlitePathPlaceholder')">
|
|
<template #prefix>
|
|
<NIcon :component="FolderOpenOutline" />
|
|
</template>
|
|
</NInput>
|
|
</NFormItem>
|
|
<NFormItem>
|
|
<NText depth="3" style="font-size: 12px;">{{ t('database.sqliteHint') }}</NText>
|
|
</NFormItem>
|
|
</template>
|
|
|
|
<!-- MySQL 配置 -->
|
|
<template v-else>
|
|
<NFormItem :label="t('database.host')">
|
|
<NInput v-model:value="form.host" placeholder="localhost" />
|
|
</NFormItem>
|
|
<NFormItem :label="t('database.port')">
|
|
<NInputNumber v-model:value="form.port" :min="1" :max="65535" style="width: 100%;" />
|
|
</NFormItem>
|
|
<NFormItem :label="t('database.database')">
|
|
<NInput v-model:value="form.database" placeholder="finance" />
|
|
</NFormItem>
|
|
<NFormItem :label="t('database.username')">
|
|
<NInput v-model:value="form.username" placeholder="root" />
|
|
</NFormItem>
|
|
<NFormItem :label="t('database.password')">
|
|
<NInput
|
|
v-model:value="form.password"
|
|
type="password"
|
|
show-password-on="click"
|
|
:placeholder="hasPassword ? t('database.passwordKeep') : t('database.passwordPlaceholder')"
|
|
/>
|
|
</NFormItem>
|
|
</template>
|
|
|
|
<NFormItem>
|
|
<NSpace>
|
|
<NButton :loading="testing" @click="handleTest">
|
|
<template #icon>
|
|
<NIcon :component="FlashOutline" />
|
|
</template>
|
|
{{ t('database.test') }}
|
|
</NButton>
|
|
<NButton type="primary" :loading="saving" @click="handleSave">
|
|
<template #icon>
|
|
<NIcon :component="SaveOutline" />
|
|
</template>
|
|
{{ t('database.save') }}
|
|
</NButton>
|
|
</NSpace>
|
|
</NFormItem>
|
|
</NForm>
|
|
</NCard>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.db-current {
|
|
margin-bottom: 16px;
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
background: rgba(0, 0, 0, 0.02);
|
|
}
|
|
</style>
|