新增:数据库可见性过滤与连接管理增强
功能: - 支持配置 MySQL/MongoDB 可见数据库列表 - 连接删除时自动清理关联数据并关闭连接池 - 新增加载数据库列表 API - 数据库错误提示优化 改进: - 代码简化:消除重复的表单验证和密码处理逻辑 - ResultPanel 表格高度计算重构 - 删除调试日志和临时文件 后端: - 新增 VisibleDatabases 字段到连接模型 - DeleteConnection 使用事务确保数据一致性 - LoadAllDatabases 支持 MySQL/MongoDB 数据库列表加载
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user