Private
Public Access
1
0

新增:数据库可见性过滤与连接管理增强

功能:
- 支持配置 MySQL/MongoDB 可见数据库列表
- 连接删除时自动清理关联数据并关闭连接池
- 新增加载数据库列表 API
- 数据库错误提示优化

改进:
- 代码简化:消除重复的表单验证和密码处理逻辑
- ResultPanel 表格高度计算重构
- 删除调试日志和临时文件

后端:
- 新增 VisibleDatabases 字段到连接模型
- DeleteConnection 使用事务确保数据一致性
- LoadAllDatabases 支持 MySQL/MongoDB 数据库列表加载
This commit is contained in:
2026-02-13 00:38:25 +08:00
parent 0229cab550
commit d62b9ca7bd
15 changed files with 993 additions and 386 deletions

View File

@@ -2,7 +2,7 @@
<a-modal
v-model:visible="visible"
title="数据库连接配置"
width="560px"
width="600px"
:body-style="{ padding: '16px 20px' }"
@cancel="handleCancel"
>
@@ -65,6 +65,67 @@
:max-length="100"
size="small"/>
</a-form-item>
<!-- 数据库过滤选项 MySQL MongoDB -->
<template v-if="form.type === 'mysql' || form.type === 'mongo'">
<a-form-item label="可见数据库" field="visibleDatabases">
<div class="database-list-container">
<!-- 顶部工具栏 -->
<div class="list-toolbar">
<a-button
type="outline"
size="small"
@click="loadAllDatabases"
:loading="loadingDatabases"
:disabled="!canLoadDatabases"
>
<template #icon>
<icon-refresh />
</template>
{{ allDatabases.length > 0 ? '重新加载' : '加载数据库列表' }}
</a-button>
<template v-if="allDatabases.length > 0">
<div class="toolbar-stats">
已选 {{ selectedDatabases.length }} / {{ allDatabases.length }}
</div>
<a-button size="small" @click="handleSelectAll(true)">全选</a-button>
<a-button size="small" @click="handleInvertSelection">反选</a-button>
<a-button size="small" @click="handleSelectAll(false)">清空</a-button>
</template>
</div>
<!-- 空状态 -->
<div v-if="!loadingDatabases && allDatabases.length === 0" class="list-empty">
<icon-storage />
<span>点击上方按钮加载数据库列表</span>
</div>
<!-- 数据库列表复选框 -->
<div v-else class="database-checkbox-list">
<div
v-for="db in allDatabases"
:key="db"
class="database-checkbox-item"
>
<a-checkbox
:model-value="selectedDatabases.includes(db)"
:disabled="loadingDatabases"
@change="(checked: boolean) => toggleDatabase(db, checked)"
>
{{ db }}
</a-checkbox>
</div>
</div>
<!-- 提示信息 -->
<div v-if="allDatabases.length > 0" class="list-tip">
<icon-info-circle />
未选择任何数据库时将展示所有数据库
</div>
</div>
</a-form-item>
</template>
<!-- MongoDB 专用选项 -->
<template v-if="form.type === 'mongo'">
@@ -87,12 +148,20 @@
</template>
<script setup lang="ts">
import {reactive, ref, watch} from 'vue'
import {reactive, ref, watch, computed, nextTick} from 'vue'
import {Message} from '@arco-design/web-vue'
import {
IconCheckCircle,
IconClose,
IconInfoCircle,
IconRefresh,
IconStorage
} from '@arco-design/web-vue/es/icon'
import {
ListDbConnections,
SaveDbConnection
} from '../../../wailsjs/wailsjs/go/main/App'
import { getConnectionFailedTip, getLoadFailedTip } from '@/utils/database-error'
// 使用 defineModel 简化 v-model:visible 双向绑定Vue 3.5+
const visible = defineModel('visible', { type: Boolean, default: false })
@@ -113,6 +182,16 @@ const errorMessage = ref('')
// 是否修改密码(编辑模式下)
const isPasswordChanged = ref(false)
// 数据库过滤相关
const loadingDatabases = ref(false)
const allDatabases = ref<string[]>([])
const selectedDatabases = ref<string[]>([])
// 是否可以加载数据库列表
const canLoadDatabases = computed(() =>
!!(form.host && form.port && form.username && (form.password || props.connectionId))
)
const form = reactive({
name: '',
type: 'mysql',
@@ -121,7 +200,8 @@ const form = reactive({
username: '',
password: '',
database: '',
options: ''
options: '',
visibleDatabases: ''
})
// 选项表单(用于表单输入)
@@ -213,17 +293,9 @@ const rules = {
// 获取密码输入框的占位符
const getPasswordPlaceholder = () => {
if (props.connectionId) {
return '请输入新密码'
}
switch (form.type) {
case 'redis':
return '可选,留空则无密码连接'
case 'mongo':
return '可选,留空则无认证连接'
default:
return '请输入密码'
}
if (props.connectionId) return '请输入新密码'
const placeholders = { redis: '可选,留空则无密码连接', mongo: '可选,留空则无认证连接' }
return placeholders[form.type] || '请输入密码'
}
// 监听类型变化,设置默认端口、主机和用户名
@@ -271,6 +343,7 @@ const handleTypeChange = (type) => {
const loadConnection = async () => {
if (!props.connectionId) {
resetForm()
// 新建模式:不自动加载,等用户手动点击
return
}
@@ -293,9 +366,28 @@ const loadConnection = async () => {
parseOptionsToForm(conn.options || '')
// 然后设置 form.options这样不会触发 watch
form.options = conn.options || ''
// 设置可见数据库
form.visibleDatabases = conn.visible_databases || ''
// 编辑模式下,默认不修改密码
form.password = ''
isPasswordChanged.value = false
// 恢复数据库选择
if (conn.visible_databases) {
try {
selectedDatabases.value = JSON.parse(conn.visible_databases)
} catch (error) {
console.warn('解析可见数据库列表失败:', error)
selectedDatabases.value = []
}
} else {
selectedDatabases.value = []
}
// 编辑模式:自动加载数据库列表
nextTick(() => {
loadAllDatabases()
})
}
} catch (error) {
console.error('加载连接详情失败:', error)
@@ -304,6 +396,28 @@ const loadConnection = async () => {
}
}
// 获取要使用的密码(编辑模式下未修改密码时为空)
const getPasswordToUse = () =>
(props.connectionId && !isPasswordChanged.value) ? '' : (form.password || '')
// 表单验证(返回 true 表示验证通过)
const validateForm = async () => {
if (!formRef.value) {
console.error('formRef 未初始化')
return false
}
errorMessage.value = ''
try {
await formRef.value.validate()
return true
} catch (error) {
const errorMsg = error?.fields?.[Object.keys(error.fields)[0]]?.[0]?.message || '请检查表单填写是否正确'
errorMessage.value = errorMsg
Message.warning(errorMsg)
return false
}
}
// 重置表单
const resetForm = () => {
form.name = ''
@@ -314,67 +428,44 @@ const resetForm = () => {
form.password = ''
form.database = ''
form.options = ''
form.visibleDatabases = ''
optionsForm.authSource = ''
isPasswordChanged.value = false
loadingDatabases.value = false
allDatabases.value = []
selectedDatabases.value = []
}
// 测试连接(不保存数据)
const handleTest = async () => {
if (!formRef.value) {
console.error('formRef 未初始化')
return
}
// 清除之前的错误信息
errorMessage.value = ''
try {
await formRef.value.validate()
// 验证通过,继续执行
} catch (error) {
// 表单验证失败
const errorFields = error?.fields || {}
const firstError = Object.values(errorFields)[0]
const errorMsg = firstError?.[0]?.message || '请检查表单填写是否正确'
errorMessage.value = errorMsg
Message.warning(errorMsg)
return
}
if (!(await validateForm())) return
// 检查 Go 后端是否可用
if (!(window as any).go?.main?.App) {
errorMessage.value = 'Go 后端未就绪,请确保应用已启动'
Message.error('Go 后端未就绪,请确保应用已启动')
const msg = 'Go 后端未就绪,请确保应用已启动'
errorMessage.value = msg
Message.error(msg)
return
}
testing.value = true
try {
// 编辑模式下,如果未修改密码,传递空字符串(后端会获取已保存的密码)
const passwordToTest = (props.connectionId && !isPasswordChanged.value) ? '' : (form.password || '')
// 合并选项为 JSON
const optionsJson = mergeOptionsToJson()
// 直接测试连接,不保存数据
await (window as any).go.main.App.TestDbConnectionWithParams({
id: props.connectionId || 0, // 编辑模式下传递ID用于获取已保存的密码
id: props.connectionId || 0,
type: form.type,
host: form.host,
port: form.port,
username: form.username || '',
password: passwordToTest,
password: getPasswordToUse(),
database: form.database || '',
options: optionsJson
options: mergeOptionsToJson()
})
Message.success('连接测试成功')
errorMessage.value = ''
} catch (error) {
console.error('连接测试失败:', error)
const errorMsg = error.message || error.toString() || '未知错误'
errorMessage.value = '连接测试失败: ' + errorMsg
Message.error('连接测试失败: ' + errorMsg)
const friendlyMsg = getConnectionFailedTip(error, form.type)
errorMessage.value = friendlyMsg
Message.error(friendlyMsg)
} finally {
testing.value = false
}
@@ -382,42 +473,18 @@ const handleTest = async () => {
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) {
console.error('formRef 未初始化')
return
}
// 清除之前的错误信息
errorMessage.value = ''
try {
await formRef.value.validate()
// 验证通过,继续执行
} catch (error) {
// 表单验证失败
const errorFields = error?.fields || {}
const firstError = Object.values(errorFields)[0]
const errorMsg = firstError?.[0]?.message || '请检查表单填写是否正确'
errorMessage.value = errorMsg
Message.warning(errorMsg)
return
}
if (!(await validateForm())) return
// 检查 Go 后端是否可用
if (!(window as any).go?.main?.App) {
errorMessage.value = 'Go 后端未就绪,请确保应用已启动'
Message.error('Go 后端未就绪,请确保应用已启动')
const msg = 'Go 后端未就绪,请确保应用已启动'
errorMessage.value = msg
Message.error(msg)
return
}
saving.value = true
try {
// 编辑模式下,如果未修改密码,传递空字符串(后端会保留原密码)
const passwordToSave = (props.connectionId && !isPasswordChanged.value) ? '' : (form.password || '')
// 合并选项为 JSON
const optionsJson = mergeOptionsToJson()
await (window as any).go.main.App.SaveDbConnection({
id: props.connectionId || 0,
name: form.name,
@@ -425,18 +492,17 @@ const handleSubmit = async () => {
host: form.host,
port: form.port,
username: form.username || '',
password: passwordToSave,
password: getPasswordToUse(),
database: form.database || '',
options: optionsJson
options: mergeOptionsToJson(),
visible_databases: form.visibleDatabases || ''
})
Message.success(props.connectionId ? '更新成功' : '保存成功')
errorMessage.value = ''
emit('success')
visible.value = false
} catch (error) {
console.error('保存失败:', error)
const errorMsg = error.message || error.toString() || '未知错误'
const errorMsg = error?.message || error?.toString() || '未知错误'
errorMessage.value = '保存失败: ' + errorMsg
Message.error('保存失败: ' + errorMsg)
} finally {
@@ -470,6 +536,83 @@ watch(
{ deep: true }
)
// 加载全部数据库列表
const loadAllDatabases = async () => {
if (!canLoadDatabases.value) {
Message.warning('请先填写连接信息')
return
}
loadingDatabases.value = true
try {
const databases = await (window as any).go.main.App.LoadAllDatabases({
id: props.connectionId || 0,
type: form.type,
host: form.host,
port: form.port,
username: form.username || '',
password: getPasswordToUse(),
database: form.database || '',
options: mergeOptionsToJson()
})
allDatabases.value = databases || []
// 从已保存的 visibleDatabases 中恢复选择
if (form.visibleDatabases) {
try {
selectedDatabases.value = JSON.parse(form.visibleDatabases)
.filter((db: string) => databases.includes(db))
} catch (error) {
console.warn('解析可见数据库列表失败:', error)
selectedDatabases.value = []
}
} else {
selectedDatabases.value = []
}
Message.success(`成功加载 ${databases.length} 个数据库`)
} catch (error) {
Message.error(getLoadFailedTip(error, 'databases'))
} finally {
loadingDatabases.value = false
}
}
// 切换单个数据库选择
const toggleDatabase = (dbName: string, checked: boolean) => {
const list = selectedDatabases.value
if (checked && !list.includes(dbName)) {
list.push(dbName)
} else if (!checked) {
selectedDatabases.value = list.filter(db => db !== dbName)
}
}
// 全选/全不选
const handleSelectAll = (selectAll: boolean) => {
if (selectAll) {
selectedDatabases.value = [...allDatabases.value]
} else {
selectedDatabases.value = []
}
}
// 反选
const handleInvertSelection = () => {
const selectedSet = new Set(selectedDatabases.value)
selectedDatabases.value = allDatabases.value.filter(db => !selectedSet.has(db))
}
// 监听数据库选择变化,同步到表单
watch(selectedDatabases, (newVal) => {
if (newVal.length === 0) {
form.visibleDatabases = ''
} else {
form.visibleDatabases = JSON.stringify(newVal)
}
}, { deep: true })
// 监听 visible 变化
watch(visible, (val) => {
if (val) {
@@ -507,5 +650,81 @@ watch(visible, (val) => {
margin-top: 4px;
display: block;
}
.database-list-container {
width: 100%;
}
/* 顶部工具栏 */
.list-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.toolbar-stats {
font-size: 13px;
color: var(--color-text-2);
margin: 0 8px;
}
/* 空状态 */
.list-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px 20px;
color: var(--color-text-3);
font-size: 13px;
}
.list-empty svg {
font-size: 32px;
}
/* 复选框列表 */
.database-checkbox-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
max-height: 240px;
overflow-y: auto;
padding: 12px;
background: var(--color-fill-2);
border: 1px solid var(--color-border-2);
border-radius: 6px;
}
.database-checkbox-item {
display: flex;
align-items: center;
}
.database-checkbox-item :deep(.arco-checkbox) {
width: 100%;
font-size: 13px;
}
/* 底部提示 */
.list-tip {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
padding: 8px 12px;
background: var(--color-fill-1);
border-radius: 4px;
font-size: 12px;
color: var(--color-text-3);
}
.list-tip svg {
color: var(--color-text-3);
font-size: 14px;
}
</style>