feat: 添加Neo4j图数据库支持及前端代码重构
- 新增 Neo4j 图数据库 handler、service、model - 后端添加 SaveGraph API 接口 - 前端 Database.vue 重构,拆分为独立组件 - 新增 web/src/views/database/ 组件目录 - 删除临时文件 (temp_*.go) - 添加 Neo4j 相关 API 需求文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,679 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface Database {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
db_type: string
|
||||
host: string
|
||||
port: number
|
||||
username: string
|
||||
password: string
|
||||
database: string
|
||||
table_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 表信息
|
||||
interface TableInfo {
|
||||
name: string
|
||||
ddl: string
|
||||
columns: ColumnInfo[]
|
||||
table_comment?: string
|
||||
}
|
||||
|
||||
// 字段信息
|
||||
interface ColumnInfo {
|
||||
name: string
|
||||
type: string
|
||||
comment: string
|
||||
mapped_name: string
|
||||
column_type?: string
|
||||
is_nullable?: string
|
||||
default_value?: string
|
||||
column_key?: string
|
||||
}
|
||||
|
||||
// 解析 DDL 获取列信息
|
||||
function parseDDLColumns(ddl: string): ColumnInfo[] {
|
||||
const columns: ColumnInfo[] = []
|
||||
if (!ddl) return columns
|
||||
|
||||
// 移除 CREATE TABLE 语句,只保留括号内的内容
|
||||
const match = ddl.match(/\(([\s\S]*)\)\s*.*$/m)
|
||||
if (!match) return columns
|
||||
|
||||
const body = match[1]
|
||||
|
||||
// 按换行或逗号分割列定义(处理多行的情况)
|
||||
const lines = body.split(/,\s*(?=`\w+`|\s*$)/)
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('PRIMARY KEY') || trimmed.startsWith('KEY ') ||
|
||||
trimmed.startsWith('UNIQUE KEY') || trimmed.startsWith('FULLTEXT') ||
|
||||
trimmed.startsWith('CONSTRAINT') || trimmed.startsWith('FOREIGN KEY') ||
|
||||
trimmed.startsWith('ENGINE') || trimmed.startsWith('CHARSET') || trimmed.startsWith(')')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析列名(支持反引号和单引号)
|
||||
const colNameMatch = trimmed.match(/^`?(\w+)`?\s+/)
|
||||
if (!colNameMatch) continue
|
||||
|
||||
const name = colNameMatch[1]
|
||||
|
||||
// 提取列定义剩余部分
|
||||
const rest = trimmed.substring(colNameMatch[0].length)
|
||||
|
||||
// 提取数据类型(到 NOT NULL / DEFAULT / COMMENT 之前)
|
||||
const typeMatch = rest.match(/^([^\s]+(?:\s*\([^\)]+\))?)/)
|
||||
const type = typeMatch ? typeMatch[1] : ''
|
||||
|
||||
// 提取 COMMENT
|
||||
const commentMatch = trimmed.match(/COMMENT\s+['"]([^'"]*)['"]/i)
|
||||
const comment = commentMatch ? commentMatch[1] : ''
|
||||
|
||||
// 提取默认值
|
||||
const defaultMatch = trimmed.match(/DEFAULT\s+([^\s,]+)/i)
|
||||
const defaultValue = defaultMatch ? defaultMatch[1] : ''
|
||||
|
||||
// 判断是否可空
|
||||
const isNullable = trimmed.includes('NOT NULL') ? 'NO' : 'YES'
|
||||
|
||||
// 判断是否是主键
|
||||
const isPrimaryKey = trimmed.includes('PRIMARY KEY')
|
||||
|
||||
columns.push({
|
||||
name,
|
||||
type,
|
||||
comment,
|
||||
mapped_name: '',
|
||||
column_type: type,
|
||||
is_nullable: isNullable,
|
||||
default_value: defaultValue,
|
||||
column_key: isPrimaryKey ? 'PRI' : '',
|
||||
})
|
||||
}
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
// 数据库数据
|
||||
const databases = ref<Database[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 编辑状态
|
||||
const editingDb = ref<Database | null>(null)
|
||||
const isEditing = ref(false)
|
||||
const isCreating = ref(false)
|
||||
const searchQuery = ref('')
|
||||
|
||||
// 表映射弹窗状态
|
||||
const isMapping = ref(false)
|
||||
const mappingDb = ref<Database | null>(null)
|
||||
const tables = ref<TableInfo[]>([])
|
||||
const tableLoading = ref(false)
|
||||
const tableSearchQuery = ref('')
|
||||
const selectedTables = ref<TableInfo[]>([]) // 选中的表列表
|
||||
const tablePage = ref(1)
|
||||
const tablePageSize = ref(10)
|
||||
const currentTableIndex = ref(0)
|
||||
|
||||
// 搜索时重置分页
|
||||
watch(tableSearchQuery, () => {
|
||||
tablePage.value = 1
|
||||
})
|
||||
|
||||
// 映射步骤:1-选择表, 2-DDl和映射
|
||||
const mappingStep = ref(1)
|
||||
|
||||
// 当前正在编辑的表
|
||||
const selectedTable = computed(() => {
|
||||
if (selectedTables.value.length === 0) return null
|
||||
return selectedTables.value[currentTableIndex.value] || null
|
||||
})
|
||||
|
||||
// 检查表是否被选中
|
||||
const isTableSelected = (tableName: string) => {
|
||||
return selectedTables.value.some(t => t.name === tableName)
|
||||
}
|
||||
|
||||
// 切换表的选择状态
|
||||
const toggleTableSelection = (table: TableInfo) => {
|
||||
const index = selectedTables.value.findIndex(t => t.name === table.name)
|
||||
if (index >= 0) {
|
||||
selectedTables.value.splice(index, 1)
|
||||
// 调整当前索引
|
||||
if (currentTableIndex.value >= selectedTables.value.length) {
|
||||
currentTableIndex.value = Math.max(0, selectedTables.value.length - 1)
|
||||
}
|
||||
} else {
|
||||
selectedTables.value.push(table)
|
||||
}
|
||||
}
|
||||
|
||||
// 全选所有表
|
||||
const selectAllTables = () => {
|
||||
selectedTables.value = [...filteredTables()]
|
||||
currentTableIndex.value = 0
|
||||
}
|
||||
|
||||
// 清除所有选择
|
||||
const clearAllTables = () => {
|
||||
selectedTables.value = []
|
||||
currentTableIndex.value = 0
|
||||
}
|
||||
|
||||
// 上一张表
|
||||
const prevTable = () => {
|
||||
if (currentTableIndex.value > 0) {
|
||||
currentTableIndex.value--
|
||||
}
|
||||
}
|
||||
|
||||
// 下一张表
|
||||
const nextTable = () => {
|
||||
if (currentTableIndex.value < selectedTables.value.length - 1) {
|
||||
currentTableIndex.value++
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤后的表
|
||||
const filteredTables = () => {
|
||||
let result = tables.value
|
||||
if (tableSearchQuery.value) {
|
||||
result = result.filter(t => t.name.toLowerCase().includes(tableSearchQuery.value.toLowerCase()))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const paginatedTables = computed(() => {
|
||||
const start = (tablePage.value - 1) * tablePageSize.value
|
||||
const end = start + tablePageSize.value
|
||||
return filteredTables().slice(start, end)
|
||||
})
|
||||
|
||||
const totalFilteredTables = computed(() => filteredTables().length)
|
||||
|
||||
// 关闭映射弹窗
|
||||
const closeMapping = () => {
|
||||
isMapping.value = false
|
||||
mappingDb.value = null
|
||||
tables.value = []
|
||||
selectedTables.value = []
|
||||
currentTableIndex.value = 0
|
||||
mappingStep.value = 1
|
||||
tableSearchQuery.value = ''
|
||||
tablePage.value = 1
|
||||
}
|
||||
|
||||
// 打开映射弹窗(从列表页面点击映射按钮)
|
||||
const openMapping = async (db: Database) => {
|
||||
mappingDb.value = db
|
||||
tables.value = []
|
||||
selectedTables.value = []
|
||||
currentTableIndex.value = 0
|
||||
mappingStep.value = 1
|
||||
tableLoading.value = true
|
||||
isMapping.value = true
|
||||
|
||||
try {
|
||||
// 并行获取实时表结构和已保存的映射数据
|
||||
const [checkRes, mappingRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/database/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
db_type: db.db_type,
|
||||
host: db.host,
|
||||
port: db.port,
|
||||
username: db.username,
|
||||
password: db.password,
|
||||
database: db.database,
|
||||
charset: 'utf8mb4',
|
||||
}),
|
||||
}),
|
||||
// 如果是已存在的数据库,获取已保存的映射
|
||||
db.id ? fetch(`${API_BASE}/sub-table/database/${db.id}`) : Promise.resolve(null),
|
||||
])
|
||||
|
||||
if (!checkRes.ok) {
|
||||
throw new Error(`HTTP ${checkRes.status}`)
|
||||
}
|
||||
|
||||
const result = await checkRes.json()
|
||||
|
||||
// 获取已保存的映射数据
|
||||
let savedMappings: any = {}
|
||||
let savedTableNames: string[] = [] // 已保存的表名列表
|
||||
if (mappingRes && mappingRes.ok) {
|
||||
const mappingResult = await mappingRes.json()
|
||||
// 后端返回的是 list 字段
|
||||
const tablesList = mappingResult.list || mappingResult.tables || []
|
||||
if (tablesList.length > 0) {
|
||||
// 构建映射表: parent_table -> fields[]
|
||||
for (const table of tablesList) {
|
||||
savedMappings[table.parent_table] = table.fields || []
|
||||
savedTableNames.push(table.parent_table)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.success && result.tables) {
|
||||
tables.value = result.tables.map((table: any) => {
|
||||
const ddl = table.ddl || ''
|
||||
// 获取该表已保存的字段映射
|
||||
const savedFields = savedMappings[table.table_name] || []
|
||||
|
||||
// 如果有 columns 数据则使用,否则尝试从 DDL 解析
|
||||
let columns: ColumnInfo[]
|
||||
if (table.columns && table.columns.length > 0) {
|
||||
columns = table.columns.map((col: any) => {
|
||||
// 查找已保存的映射
|
||||
const savedField = savedFields.find((f: any) => f.column_name === col.column_name)
|
||||
return {
|
||||
name: col.column_name,
|
||||
type: col.data_type || col.column_type || '',
|
||||
column_type: col.column_type || '',
|
||||
comment: col.column_comment || '',
|
||||
is_nullable: col.is_nullable || '',
|
||||
default_value: col.default_value || '',
|
||||
column_key: col.column_key || '',
|
||||
mapped_name: savedField?.mapped_name || col.mapped_name || '',
|
||||
}
|
||||
})
|
||||
} else if (ddl) {
|
||||
// 从 DDL 解析列信息,并合并已保存的映射
|
||||
columns = parseDDLColumns(ddl).map(col => {
|
||||
const savedField = savedFields.find((f: any) => f.column_name === col.name)
|
||||
return {
|
||||
...col,
|
||||
mapped_name: savedField?.mapped_name || '',
|
||||
}
|
||||
})
|
||||
} else {
|
||||
columns = []
|
||||
}
|
||||
|
||||
return {
|
||||
name: table.table_name,
|
||||
table_comment: table.table_comment || '',
|
||||
ddl,
|
||||
columns,
|
||||
}
|
||||
})
|
||||
|
||||
// 恢复已选择的表
|
||||
if (savedTableNames.length > 0) {
|
||||
selectedTables.value = tables.value.filter(t => savedTableNames.includes(t.name))
|
||||
} else {
|
||||
selectedTables.value = []
|
||||
}
|
||||
currentTableIndex.value = 0
|
||||
} else {
|
||||
ElMessage.warning(result.message || '获取表结构失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取表结构失败:', error)
|
||||
ElMessage.error('获取表结构失败: ' + error.message)
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 进入下一步(选择表后)
|
||||
const goToStep2 = () => {
|
||||
if (selectedTables.value.length > 0) {
|
||||
mappingStep.value = 2
|
||||
currentTableIndex.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一步
|
||||
const goToStep1 = () => {
|
||||
mappingStep.value = 1
|
||||
}
|
||||
|
||||
// 保存映射(创建或更新数据库 + 保存子表)
|
||||
const saveMapping = async () => {
|
||||
if (!mappingDb.value) {
|
||||
ElMessage.warning('数据库信息不存在')
|
||||
return
|
||||
}
|
||||
|
||||
const isEditing = !!mappingDb.value.id
|
||||
const subTablesData = selectedTables.value.map(table => ({
|
||||
parent_table: table.name,
|
||||
sub_table_name: table.table_comment || table.name,
|
||||
sub_table_comment: table.table_comment || '',
|
||||
fields: (table.columns || []).map(col => ({
|
||||
column_name: col.name,
|
||||
mapped_name: col.mapped_name || '', // 这里实际存的是 comment
|
||||
})),
|
||||
}))
|
||||
|
||||
try {
|
||||
let response: Response
|
||||
|
||||
if (isEditing) {
|
||||
// 编辑模式:调用更新接口
|
||||
response = await fetch(`${API_BASE}/database/${mappingDb.value.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: mappingDb.value.name,
|
||||
description: mappingDb.value.description,
|
||||
db_type: mappingDb.value.db_type,
|
||||
host: mappingDb.value.host,
|
||||
port: mappingDb.value.port,
|
||||
username: mappingDb.value.username,
|
||||
password: mappingDb.value.password,
|
||||
database: mappingDb.value.database,
|
||||
sub_tables: subTablesData,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
// 新建模式:调用创建接口
|
||||
response = await fetch(`${API_BASE}/database/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: mappingDb.value.name,
|
||||
description: mappingDb.value.description,
|
||||
db_type: mappingDb.value.db_type,
|
||||
host: mappingDb.value.host,
|
||||
port: mappingDb.value.port,
|
||||
username: mappingDb.value.username,
|
||||
password: mappingDb.value.password,
|
||||
database: mappingDb.value.database,
|
||||
sub_tables: subTablesData,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
if (!isEditing) {
|
||||
databases.value.push(result)
|
||||
}
|
||||
closeMapping()
|
||||
ElMessage.success(isEditing ? 'Mapping updated successfully' : 'Database created successfully')
|
||||
// 刷新列表
|
||||
fetchDatabases()
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
ElMessage.error(errorData.error || (isEditing ? 'Failed to update mapping' : 'Failed to create database'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error('保存失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const dbTypes = ['MySQL', 'PostgreSQL', 'MongoDB', 'Redis']
|
||||
|
||||
// API 基础 URL
|
||||
const API_BASE = 'http://localhost:8082'
|
||||
|
||||
// 获取列表数据
|
||||
const fetchDatabases = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/database/list`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch databases')
|
||||
}
|
||||
const data = await response.json()
|
||||
databases.value = data.list || []
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch databases:', error)
|
||||
ElMessage.error('Failed to load databases')
|
||||
databases.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDatabases()
|
||||
})
|
||||
|
||||
// 新建 Database 表单
|
||||
const newDbForm = ref({
|
||||
name: '',
|
||||
db_type: 'MySQL',
|
||||
description: '',
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
username: 'root',
|
||||
password: '',
|
||||
database: '',
|
||||
})
|
||||
|
||||
// 打开新建弹窗
|
||||
const openCreate = () => {
|
||||
newDbForm.value = {
|
||||
name: '',
|
||||
db_type: 'MySQL',
|
||||
description: '',
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
username: 'root',
|
||||
password: '',
|
||||
database: '',
|
||||
}
|
||||
isCreating.value = true
|
||||
}
|
||||
|
||||
// 关闭新建弹窗
|
||||
const closeCreate = () => {
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
// 测试连接并获取表列表
|
||||
const testConnect = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/database/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
db_type: newDbForm.value.db_type,
|
||||
host: newDbForm.value.host,
|
||||
port: newDbForm.value.port,
|
||||
username: newDbForm.value.username,
|
||||
password: newDbForm.value.password,
|
||||
database: newDbForm.value.database,
|
||||
charset: 'utf8mb4',
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
// 连接成功,显示表映射弹窗
|
||||
const dbInfo: Database = {
|
||||
id: '',
|
||||
name: newDbForm.value.name || 'Untitled Database',
|
||||
description: newDbForm.value.description,
|
||||
db_type: newDbForm.value.db_type,
|
||||
host: newDbForm.value.host,
|
||||
port: newDbForm.value.port,
|
||||
username: newDbForm.value.username,
|
||||
password: newDbForm.value.password,
|
||||
database: newDbForm.value.database,
|
||||
table_count: 0,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
}
|
||||
|
||||
// 将后端返回的表数据映射到前端格式
|
||||
if (result.tables && result.tables.length > 0) {
|
||||
tables.value = result.tables.map((table: any) => {
|
||||
const ddl = table.ddl || ''
|
||||
// 如果有 columns 数据则使用,否则尝试从 DDL 解析
|
||||
let columns: ColumnInfo[]
|
||||
if (table.columns && table.columns.length > 0) {
|
||||
columns = table.columns.map((col: any) => ({
|
||||
name: col.column_name,
|
||||
type: col.data_type || col.column_type || '',
|
||||
column_type: col.column_type || '',
|
||||
comment: col.column_comment || '',
|
||||
is_nullable: col.is_nullable || '',
|
||||
default_value: col.default_value || '',
|
||||
column_key: col.column_key || '',
|
||||
mapped_name: '',
|
||||
}))
|
||||
} else if (ddl) {
|
||||
// 从 DDL 解析列信息
|
||||
columns = parseDDLColumns(ddl)
|
||||
} else {
|
||||
columns = []
|
||||
}
|
||||
|
||||
return {
|
||||
name: table.table_name,
|
||||
table_comment: table.table_comment || '',
|
||||
ddl,
|
||||
columns,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 默认不选中任何表
|
||||
selectedTables.value = []
|
||||
currentTableIndex.value = 0
|
||||
mappingStep.value = 1
|
||||
mappingDb.value = dbInfo
|
||||
isMapping.value = true
|
||||
isCreating.value = false // 关闭创建弹窗
|
||||
ElMessage.success('Connection successful! Please select tables to map.')
|
||||
} else {
|
||||
ElMessage.error(result.message || 'Connection failed')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Connection test failed:', error)
|
||||
ElMessage.error('Connection test failed: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑表单数据
|
||||
const editForm = ref({
|
||||
name: '',
|
||||
db_type: '',
|
||||
description: '',
|
||||
host: '',
|
||||
port: 0,
|
||||
username: '',
|
||||
password: '',
|
||||
database: '',
|
||||
})
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEdit = (db: Database) => {
|
||||
editingDb.value = db
|
||||
editForm.value = {
|
||||
name: db.name,
|
||||
db_type: db.db_type,
|
||||
description: db.description,
|
||||
host: db.host,
|
||||
port: db.port,
|
||||
username: db.username,
|
||||
password: db.password,
|
||||
database: db.database,
|
||||
}
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
// 保存编辑
|
||||
const saveEdit = async () => {
|
||||
if (editingDb.value) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/database/${editingDb.value.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: editForm.value.name,
|
||||
description: editForm.value.description,
|
||||
db_type: editForm.value.db_type,
|
||||
host: editForm.value.host,
|
||||
port: editForm.value.port,
|
||||
username: editForm.value.username,
|
||||
password: editForm.value.password,
|
||||
database: editForm.value.database,
|
||||
}),
|
||||
})
|
||||
if (response.ok) {
|
||||
const updatedDb = await response.json()
|
||||
const index = databases.value.findIndex(d => d.id === editingDb.value!.id)
|
||||
if (index !== -1) {
|
||||
databases.value[index] = updatedDb
|
||||
}
|
||||
ElMessage.success('Database updated successfully')
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
ElMessage.error(errorData.error || 'Failed to update database')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update database:', error)
|
||||
ElMessage.error('Failed to update database')
|
||||
}
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false
|
||||
editingDb.value = null
|
||||
}
|
||||
|
||||
// 删除 Database
|
||||
const deleteDb = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/database/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
if (response.ok) {
|
||||
databases.value = databases.value.filter(d => d.id !== id)
|
||||
ElMessage.success('Database deleted successfully')
|
||||
} else {
|
||||
ElMessage.error('Failed to delete database')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete database:', error)
|
||||
ElMessage.error('Failed to delete database')
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤后的 Databases
|
||||
const filteredDatabases = () => {
|
||||
return databases.value.filter(db => {
|
||||
const matchSearch = !searchQuery.value ||
|
||||
db.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
db.db_type.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
db.host.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
return matchSearch
|
||||
})
|
||||
}
|
||||
|
||||
import { useDatabase } from './database/useDatabase'
|
||||
import './database/database.css'
|
||||
|
||||
const {
|
||||
// State
|
||||
databases,
|
||||
loading,
|
||||
editingDb,
|
||||
isEditing,
|
||||
isCreating,
|
||||
searchQuery,
|
||||
isMapping,
|
||||
mappingDb,
|
||||
tables,
|
||||
tableLoading,
|
||||
tableSearchQuery,
|
||||
selectedTables,
|
||||
tablePage,
|
||||
tablePageSize,
|
||||
currentTableIndex,
|
||||
mappingStep,
|
||||
newDbForm,
|
||||
editForm,
|
||||
dbTypes,
|
||||
// Computed
|
||||
selectedTable,
|
||||
filteredDatabases,
|
||||
paginatedTables,
|
||||
totalFilteredTables,
|
||||
// Methods
|
||||
isTableSelected,
|
||||
toggleTableSelection,
|
||||
selectAllTables,
|
||||
clearAllTables,
|
||||
prevTable,
|
||||
nextTable,
|
||||
fetchDatabases,
|
||||
openCreate,
|
||||
closeCreate,
|
||||
closeMapping,
|
||||
testConnect,
|
||||
openMapping,
|
||||
goToStep2,
|
||||
goToStep1,
|
||||
saveMapping,
|
||||
openEdit,
|
||||
saveEdit,
|
||||
cancelEdit,
|
||||
deleteDb,
|
||||
} = useDatabase()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -979,6 +354,57 @@ const filteredDatabases = () => {
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接凭据 - 当选择Neo4j时显示 -->
|
||||
<div v-if="newDbForm.db_type === 'Neo4j'" class="space-y-4 pt-2">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Host</label>
|
||||
<input
|
||||
v-model="newDbForm.host"
|
||||
type="text"
|
||||
placeholder="localhost"
|
||||
class="input-field"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
|
||||
<input
|
||||
v-model="newDbForm.port"
|
||||
type="number"
|
||||
placeholder="7687"
|
||||
class="input-field"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Database</label>
|
||||
<input
|
||||
v-model="newDbForm.database"
|
||||
type="text"
|
||||
placeholder="neo4j (default)"
|
||||
class="input-field"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">User</label>
|
||||
<input
|
||||
v-model="newDbForm.username"
|
||||
type="text"
|
||||
placeholder="neo4j"
|
||||
class="input-field"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||||
<input
|
||||
v-model="newDbForm.password"
|
||||
type="password"
|
||||
placeholder="Enter password..."
|
||||
class="input-field"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
|
||||
Reference in New Issue
Block a user