新增:连接管理、数据查询等功能
This commit is contained in:
511
web/src/views/db-cli/components/ConnectionForm.vue
Normal file
511
web/src/views/db-cli/components/ConnectionForm.vue
Normal file
@@ -0,0 +1,511 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
title="数据库连接配置"
|
||||
width="560px"
|
||||
:body-style="{ padding: '16px 20px' }"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<!-- 错误提示区域 -->
|
||||
<a-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
closable
|
||||
@close="errorMessage = ''"
|
||||
class="error-alert"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</a-alert>
|
||||
|
||||
<a-form :model="form" :rules="rules" ref="formRef" layout="horizontal" :label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 18 }" size="small">
|
||||
<a-form-item label="连接名称" field="name">
|
||||
<a-input v-model="form.name" placeholder="请输入连接名称" size="small"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="数据库类型" field="type">
|
||||
<a-select v-model="form.type" placeholder="请选择数据库类型" @change="handleTypeChange" size="small">
|
||||
<a-option value="mysql">MySQL</a-option>
|
||||
<a-option value="redis">Redis</a-option>
|
||||
<a-option value="mongo">MongoDB</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="主机地址" field="host">
|
||||
<a-input v-model="form.host" placeholder="请输入主机地址" size="small"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="端口" field="port">
|
||||
<a-input-number v-model="form.port" :min="1" :max="65535" placeholder="请输入端口" style="width: 100%"
|
||||
size="small"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="用户名" field="username" v-if="form.type !== 'redis'">
|
||||
<a-input v-model="form.username" placeholder="请输入用户名" size="small"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="密码" field="password">
|
||||
<div v-if="props.connectionId && !isPasswordChanged" class="password-display">
|
||||
<a-input
|
||||
value="已保存的密码"
|
||||
disabled
|
||||
class="password-input"
|
||||
size="small"
|
||||
/>
|
||||
<a-button type="text" size="mini" @click="isPasswordChanged = true">
|
||||
修改密码
|
||||
</a-button>
|
||||
</div>
|
||||
<a-input-password
|
||||
v-else
|
||||
v-model="form.password"
|
||||
:placeholder="getPasswordPlaceholder()"
|
||||
size="small"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="form.type === 'redis' ? '数据库编号' : '数据库名'" field="database">
|
||||
<a-input v-model="form.database"
|
||||
:placeholder="form.type === 'redis' ? 'Redis DB 编号 (0-15,默认为0)' : '可选,留空则连接所有数据库'"
|
||||
:max-length="100"
|
||||
size="small"/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- MongoDB 专用选项 -->
|
||||
<template v-if="form.type === 'mongo'">
|
||||
<a-form-item label="认证数据库" field="options.authSource">
|
||||
<a-input v-model="optionsForm.authSource" placeholder="留空则使用 admin" size="small"/>
|
||||
<template #extra>
|
||||
<span class="form-item-extra">MongoDB 用户所在的数据库,通常为 admin(可选)</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-space size="small">
|
||||
<a-button @click="handleTest" :loading="testing" size="small">测试连接</a-button>
|
||||
<a-button @click="handleCancel" size="small">取消</a-button>
|
||||
<a-button type="primary" @click="handleSubmit" :loading="saving" size="small">保存</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {reactive, ref, watch} from 'vue'
|
||||
import {Message} from '@arco-design/web-vue'
|
||||
import {
|
||||
ListDbConnections,
|
||||
SaveDbConnection
|
||||
} from '../../../wailsjs/wailsjs/go/main/App'
|
||||
|
||||
// 使用 defineModel 简化 v-model:visible 双向绑定(Vue 3.5+)
|
||||
const visible = defineModel('visible', { type: Boolean, default: false })
|
||||
|
||||
// 使用 TypeScript 泛型语法(Vue 3.5+)
|
||||
const props = defineProps<{
|
||||
connectionId?: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const formRef = ref<any>(null)
|
||||
const saving = ref(false)
|
||||
const testing = ref(false)
|
||||
const errorMessage = ref('')
|
||||
// 是否修改密码(编辑模式下)
|
||||
const isPasswordChanged = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
username: '',
|
||||
password: '',
|
||||
database: '',
|
||||
options: ''
|
||||
})
|
||||
|
||||
// 选项表单(用于表单输入)
|
||||
const optionsForm = reactive({
|
||||
authSource: ''
|
||||
})
|
||||
|
||||
// 将 options JSON 字符串解析为 optionsForm
|
||||
const parseOptionsToForm = (optionsStr: string) => {
|
||||
if (!optionsStr || optionsStr.trim() === '') {
|
||||
optionsForm.authSource = ''
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const opts = JSON.parse(optionsStr)
|
||||
optionsForm.authSource = opts.authSource || ''
|
||||
// 认证机制使用自动检测,不需要从选项读取
|
||||
} catch (error) {
|
||||
console.warn('解析 Options JSON 失败:', error)
|
||||
// 解析失败时,清空表单选项
|
||||
optionsForm.authSource = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 将 optionsForm 和 form.options 合并为 JSON 字符串
|
||||
const mergeOptionsToJson = (): string => {
|
||||
let customOptions: any = {}
|
||||
|
||||
// 先解析已有的 JSON(可能包含其他自定义选项)
|
||||
if (form.options && form.options.trim() !== '') {
|
||||
try {
|
||||
customOptions = JSON.parse(form.options)
|
||||
} catch (error) {
|
||||
console.warn('解析自定义 Options JSON 失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据数据库类型合并表单选项(仅 MongoDB)
|
||||
if (form.type === 'mongo') {
|
||||
// 只有认证数据库不为空时才添加
|
||||
if (optionsForm.authSource && optionsForm.authSource.trim() !== '') {
|
||||
customOptions.authSource = optionsForm.authSource.trim()
|
||||
}
|
||||
// 认证机制使用自动检测,不需要添加到选项
|
||||
}
|
||||
|
||||
// 如果没有任何选项,返回空字符串
|
||||
if (Object.keys(customOptions).length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return JSON.stringify(customOptions)
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
name: [
|
||||
{required: true, message: '请输入连接名称'},
|
||||
{maxLength: 100, message: '连接名称长度不能超过100个字符'}
|
||||
],
|
||||
type: [{required: true, message: '请选择数据库类型'}],
|
||||
host: [
|
||||
{required: true, message: '请输入主机地址'},
|
||||
{maxLength: 255, message: '主机地址长度不能超过255个字符'}
|
||||
],
|
||||
port: [
|
||||
{required: true, message: '请输入端口'},
|
||||
{
|
||||
validator: (value, callback) => {
|
||||
if (!value || value < 1 || value > 65535) {
|
||||
callback('端口号必须在1-65535之间')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
database: [
|
||||
{
|
||||
validator: (value, callback) => {
|
||||
// MySQL 类型时数据库名为可选(允许为空)
|
||||
// MongoDB 和 Redis 也为可选
|
||||
callback()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 获取密码输入框的占位符
|
||||
const getPasswordPlaceholder = () => {
|
||||
if (props.connectionId) {
|
||||
return '请输入新密码'
|
||||
}
|
||||
switch (form.type) {
|
||||
case 'redis':
|
||||
return '可选,留空则无密码连接'
|
||||
case 'mongo':
|
||||
return '可选,留空则无认证连接'
|
||||
default:
|
||||
return '请输入密码'
|
||||
}
|
||||
}
|
||||
|
||||
// 监听类型变化,设置默认端口、主机和用户名
|
||||
const handleTypeChange = (type) => {
|
||||
// 如果主机为空,设置默认值
|
||||
if (!form.host || form.host.trim() === '') {
|
||||
form.host = '127.0.0.1'
|
||||
}
|
||||
|
||||
// 根据类型设置默认端口和用户名
|
||||
switch (type) {
|
||||
case 'mysql':
|
||||
form.port = 3306
|
||||
if (!form.username) {
|
||||
form.username = 'root'
|
||||
}
|
||||
// 清空 MongoDB 专用选项
|
||||
optionsForm.authSource = ''
|
||||
form.options = ''
|
||||
break
|
||||
case 'redis':
|
||||
form.port = 6379
|
||||
form.username = '' // Redis 不需要用户名
|
||||
if (!form.database) {
|
||||
form.database = '0' // Redis 默认 DB 0
|
||||
}
|
||||
// 清空其他数据库的选项
|
||||
optionsForm.authSource = ''
|
||||
form.options = ''
|
||||
break
|
||||
case 'mongo':
|
||||
case 'mongodb':
|
||||
form.port = 27017
|
||||
if (!form.username) {
|
||||
form.username = 'admin' // MongoDB 常用默认用户名
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 类型变化时,同步更新 options JSON
|
||||
form.options = mergeOptionsToJson()
|
||||
}
|
||||
|
||||
// 加载连接详情
|
||||
const loadConnection = async () => {
|
||||
if (!props.connectionId) {
|
||||
resetForm()
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
if (!(window as any).go?.main?.App?.ListDbConnections) {
|
||||
return
|
||||
}
|
||||
|
||||
const connections = await (window as any).go.main.App.ListDbConnections()
|
||||
const conn = connections.find(c => c.id === props.connectionId)
|
||||
if (conn) {
|
||||
form.name = conn.name
|
||||
form.type = conn.type
|
||||
form.host = conn.host || '127.0.0.1'
|
||||
form.port = conn.port || (conn.type === 'mysql' ? 3306 : conn.type === 'redis' ? 6379 : 27017)
|
||||
form.username = conn.username || ''
|
||||
form.database = conn.database || ''
|
||||
// 先解析 options 到表单
|
||||
parseOptionsToForm(conn.options || '')
|
||||
// 然后设置 form.options(这样不会触发 watch)
|
||||
form.options = conn.options || ''
|
||||
// 编辑模式下,默认不修改密码
|
||||
form.password = ''
|
||||
isPasswordChanged.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载连接详情失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.name = ''
|
||||
form.type = 'mysql'
|
||||
form.host = '127.0.0.1'
|
||||
form.port = 3306
|
||||
form.username = 'root' // MySQL 默认用户名
|
||||
form.password = ''
|
||||
form.database = ''
|
||||
form.options = ''
|
||||
optionsForm.authSource = ''
|
||||
isPasswordChanged.value = false
|
||||
}
|
||||
|
||||
// 测试连接(不保存数据)
|
||||
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
|
||||
}
|
||||
|
||||
// 检查 Go 后端是否可用
|
||||
if (!(window as any).go?.main?.App) {
|
||||
errorMessage.value = 'Go 后端未就绪,请确保应用已启动'
|
||||
Message.error('Go 后端未就绪,请确保应用已启动')
|
||||
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,用于获取已保存的密码
|
||||
type: form.type,
|
||||
host: form.host,
|
||||
port: form.port,
|
||||
username: form.username || '',
|
||||
password: passwordToTest,
|
||||
database: form.database || '',
|
||||
options: optionsJson
|
||||
})
|
||||
|
||||
Message.success('连接测试成功')
|
||||
errorMessage.value = ''
|
||||
} catch (error) {
|
||||
console.error('连接测试失败:', error)
|
||||
const errorMsg = error.message || error.toString() || '未知错误'
|
||||
errorMessage.value = '连接测试失败: ' + errorMsg
|
||||
Message.error('连接测试失败: ' + errorMsg)
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
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
|
||||
}
|
||||
|
||||
// 检查 Go 后端是否可用
|
||||
if (!(window as any).go?.main?.App) {
|
||||
errorMessage.value = 'Go 后端未就绪,请确保应用已启动'
|
||||
Message.error('Go 后端未就绪,请确保应用已启动')
|
||||
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,
|
||||
type: form.type,
|
||||
host: form.host,
|
||||
port: form.port,
|
||||
username: form.username || '',
|
||||
password: passwordToSave,
|
||||
database: form.database || '',
|
||||
options: optionsJson
|
||||
})
|
||||
|
||||
Message.success(props.connectionId ? '更新成功' : '保存成功')
|
||||
errorMessage.value = ''
|
||||
emit('success')
|
||||
visible.value = false
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
const errorMsg = error.message || error.toString() || '未知错误'
|
||||
errorMessage.value = '保存失败: ' + errorMsg
|
||||
Message.error('保存失败: ' + errorMsg)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
errorMessage.value = ''
|
||||
visible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 是否正在加载连接(用于避免加载时触发 watch)
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 监听 optionsForm 变化,自动同步到 form.options(仅 MongoDB)
|
||||
watch(
|
||||
() => [optionsForm.authSource, form.type],
|
||||
() => {
|
||||
// 如果正在加载,不触发更新
|
||||
if (isLoading.value) {
|
||||
return
|
||||
}
|
||||
// 仅 MongoDB 需要同步选项
|
||||
if (visible.value && form.type === 'mongo') {
|
||||
form.options = mergeOptionsToJson()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (val) => {
|
||||
if (val) {
|
||||
errorMessage.value = ''
|
||||
loadConnection()
|
||||
} else {
|
||||
errorMessage.value = ''
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.password-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.options-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-item-extra {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user