新增:连接管理、数据查询等功能
This commit is contained in:
966
web/src/views/db-cli/index.vue
Normal file
966
web/src/views/db-cli/index.vue
Normal file
@@ -0,0 +1,966 @@
|
||||
<template>
|
||||
<a-layout class="db-cli-layout">
|
||||
<!-- 左侧:数据库列表视图 -->
|
||||
<a-layout-sider :width="280" class="sidebar">
|
||||
<div class="sidebar-container">
|
||||
<ConnectionTree
|
||||
:current-connection-id="currentConnection?.id"
|
||||
@connection-select="handleConnectionSelect"
|
||||
@connection-edit="handleConnectionEdit"
|
||||
@connection-delete="handleConnectionDelete"
|
||||
@connection-refresh="handleConnectionRefresh"
|
||||
@connection-test="handleConnectionTest"
|
||||
@table-select="handleTableSelect"
|
||||
@table-structure="handleTableStructure"
|
||||
@create-table="handleCreateTable"
|
||||
@new-connection="handleNewConnection"
|
||||
ref="connectionTreeRef"
|
||||
/>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 右侧:编辑器区域和结果区域 -->
|
||||
<a-layout ref="mainLayoutRef" class="main-layout">
|
||||
<!-- SQL编辑器区域 -->
|
||||
<a-layout-content
|
||||
v-if="editorVisible"
|
||||
ref="editorAreaRef"
|
||||
class="editor-area"
|
||||
:style="editorAreaStyle"
|
||||
>
|
||||
<SqlEditor
|
||||
:current-connection="currentConnection"
|
||||
@execute="handleExecuteSQL"
|
||||
@execute-selected="handleExecuteSQL"
|
||||
ref="sqlEditorRef"
|
||||
/>
|
||||
</a-layout-content>
|
||||
|
||||
<!-- 编辑器/结果分隔条 -->
|
||||
<div v-if="editorVisible" class="editor-result-divider" @mousedown="handleEditorResultDividerMouseDown">
|
||||
<a-button
|
||||
type="text"
|
||||
size="mini"
|
||||
class="divider-toggle-btn"
|
||||
@click.stop="toggleEditor"
|
||||
@mousedown.stop
|
||||
title="隐藏编辑器"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-down/>
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 编辑器隐藏时的展开按钮 -->
|
||||
<div v-if="!editorVisible" class="editor-result-divider collapsed">
|
||||
<a-button type="text" size="mini" class="divider-toggle-btn" @click="toggleEditor" title="显示编辑器">
|
||||
<template #icon>
|
||||
<icon-up/>
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果展示区域 -->
|
||||
<a-layout-content class="result-area">
|
||||
<ResultPanel
|
||||
ref="resultPanelRef"
|
||||
:loading="resultLoading"
|
||||
:error="resultError"
|
||||
:data="(resultData as unknown[] | undefined)"
|
||||
:mode="resultMode"
|
||||
@re-execute-sql="handleReExecuteSQL"
|
||||
:stats="(resultStats as { rowsAffected: number; executionTime: number } | undefined)"
|
||||
:columns="resultColumns"
|
||||
:messages="messages"
|
||||
:editor-visible="editorVisible"
|
||||
:structure-loading="structureLoading"
|
||||
:structure-error="structureError"
|
||||
:structure-data="structureData"
|
||||
:structure-info="structureInfo || undefined"
|
||||
:edit-mode="structureEditMode"
|
||||
:edited-columns="editedColumns"
|
||||
:edited-indexes="editedIndexes"
|
||||
@toggle-editor="toggleEditor"
|
||||
@update-columns="handleUpdateColumns"
|
||||
@update-indexes="handleUpdateIndexes"
|
||||
@refresh-structure="structureStore.refreshStructure"
|
||||
@switch-to-edit-mode="handleSwitchToEditMode"
|
||||
@switch-to-view-mode="handleSwitchToViewMode"
|
||||
@save-structure="handleSaveStructure"
|
||||
@cancel-edit="handleCancelEdit"
|
||||
@add-column="handleAddColumn"
|
||||
:create-info="createInfo"
|
||||
:create-loading="createLoading"
|
||||
@cancel-create="handleCancelCreate"
|
||||
@create-table="handleCreateTableSubmit"
|
||||
@tab-change="handleTabChange"
|
||||
@view-history="handleViewHistory"
|
||||
/>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
|
||||
<!-- 连接管理表单 -->
|
||||
<ConnectionForm
|
||||
v-model:visible="showConnectionForm"
|
||||
:connection-id="editingConnectionId || undefined"
|
||||
@success="handleConnectionSuccess"
|
||||
/>
|
||||
|
||||
<!-- SQL 预览确认对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="showSqlPreviewModal"
|
||||
title="确认执行表结构变更"
|
||||
:width="800"
|
||||
:mask-closable="false"
|
||||
@cancel="showSqlPreviewModal = false"
|
||||
@ok="handleConfirmSqlExecute"
|
||||
okText="确定执行"
|
||||
cancelText="取消"
|
||||
>
|
||||
<SqlPreviewDialog
|
||||
v-if="sqlPreviewStatements.length > 0"
|
||||
:statements="sqlPreviewStatements"
|
||||
:db-type="sqlPreviewDbType"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, provide, computed, nextTick, onMounted, onUnmounted, h, onBeforeUpdate } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconUp, IconDown, IconCopy } from '@arco-design/web-vue/es/icon'
|
||||
import SqlPreviewDialog from './components/SqlPreviewDialog.vue'
|
||||
import ConnectionTree from './components/ConnectionTree.vue'
|
||||
import SqlEditor from './components/SqlEditor.vue'
|
||||
import ResultPanel from './components/ResultPanel.vue'
|
||||
import ConnectionForm from './components/ConnectionForm.vue'
|
||||
import { useDbConnection } from './composables/useDbConnection'
|
||||
import { useEditorState } from './composables/useEditorState'
|
||||
import { useResultState } from './composables/useResultState'
|
||||
import { useMessageLog } from './composables/useMessageLog'
|
||||
import { useSqlExecution, DbCliKeys } from './composables/useSqlExecution'
|
||||
import { useStructureStore } from './composables/useStructureStore'
|
||||
import { useStructureEdit, type SaveStructureResult } from './composables/useStructureEdit'
|
||||
import { useCreateState } from './composables/useCreateState'
|
||||
import { createResizeHandler } from './utils/resize'
|
||||
import { STORAGE_KEYS } from './constants/storage'
|
||||
import { executeQuery } from '@/api'
|
||||
|
||||
// 类型声明
|
||||
declare global {
|
||||
interface Window {
|
||||
go?: {
|
||||
main?: {
|
||||
App?: {
|
||||
GetTableStructure?: (connectionId: number, database: string, tableName: string) => Promise<any>
|
||||
TestDbConnection?: (connectionId: number) => Promise<void>
|
||||
ExecuteSQL?: (connectionId: number, sql: string, database?: string) => Promise<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
runtime?: {
|
||||
EventsOn?: (event: string, callback: () => void) => void
|
||||
EventsOff?: (event: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 Composables
|
||||
const {
|
||||
currentConnection,
|
||||
selectedDatabase,
|
||||
showConnectionForm,
|
||||
editingConnectionId,
|
||||
selectConnection,
|
||||
editConnection,
|
||||
deleteConnection: deleteConnectionAction,
|
||||
newConnection,
|
||||
onConnectionSuccess
|
||||
} = useDbConnection()
|
||||
|
||||
const { editorVisible, toggleEditor } = useEditorState()
|
||||
|
||||
const resultState = useResultState()
|
||||
const {
|
||||
resultLoading,
|
||||
resultError,
|
||||
resultData,
|
||||
resultMode,
|
||||
resultStats,
|
||||
resultColumns,
|
||||
clearResults
|
||||
} = resultState
|
||||
|
||||
const messageLog = useMessageLog()
|
||||
const { messages, addMessage } = messageLog
|
||||
|
||||
// 提供依赖注入(供子组件使用)
|
||||
provide(DbCliKeys.resultState, resultState)
|
||||
provide(DbCliKeys.messageLog, messageLog)
|
||||
|
||||
// 在当前组件中直接传递参数(provide/inject 用于子组件,当前组件直接传参)
|
||||
const { executeSQL } = useSqlExecution(resultState, messageLog)
|
||||
|
||||
// 新架构:使用单例 Store(事件驱动)
|
||||
const structureStore = useStructureStore()
|
||||
// 直接使用 Store 的状态(Store 暴露的是 ref,在模板中自动解包)
|
||||
// 为了类型安全,使用 computed 包装
|
||||
const structureLoading = computed(() => structureStore.loading.value)
|
||||
const structureError = computed(() => structureStore.error.value)
|
||||
const structureData = computed(() => structureStore.data.value)
|
||||
const structureInfo = computed(() => structureStore.info.value)
|
||||
|
||||
// 表结构编辑状态
|
||||
const structureEdit = useStructureEdit()
|
||||
const {
|
||||
editMode: structureEditMode,
|
||||
editedColumns,
|
||||
editedIndexes,
|
||||
switchToEditMode,
|
||||
switchToViewMode,
|
||||
previewTableStructure,
|
||||
saveStructure: saveStructureEdit,
|
||||
addColumn,
|
||||
removeColumn
|
||||
} = structureEdit
|
||||
|
||||
// 表创建状态
|
||||
const createState = useCreateState()
|
||||
const {
|
||||
createInfo,
|
||||
createLoading,
|
||||
startCreate,
|
||||
cancelCreate
|
||||
} = createState
|
||||
|
||||
// 组件引用
|
||||
const connectionTreeRef = ref<any>(null)
|
||||
const sqlEditorRef = ref<any>(null)
|
||||
const resultPanelRef = ref<any>(null)
|
||||
const mainLayoutRef = ref<any>(null)
|
||||
const editorAreaRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// SQL 预览对话框状态
|
||||
const showSqlPreviewModal = ref(false)
|
||||
const sqlPreviewStatements = ref<string[]>([])
|
||||
const sqlPreviewDbType = ref<'mysql' | 'mongo' | 'redis'>('mysql')
|
||||
const sqlPreviewInfo = ref<{
|
||||
connectionId: number
|
||||
database: string
|
||||
tableName: string
|
||||
dbType: 'mysql' | 'mongo' | 'redis'
|
||||
} | null>(null)
|
||||
|
||||
|
||||
// 编辑器/结果区域高度调整
|
||||
const loadEditorAreaHeight = (): number => {
|
||||
const saved = localStorage.getItem(STORAGE_KEYS.EDITOR_AREA_HEIGHT)
|
||||
return saved ? Number(saved) : 50
|
||||
}
|
||||
const editorAreaHeight = ref(loadEditorAreaHeight())
|
||||
const editorAreaPixelHeight = ref<number | null>(null)
|
||||
|
||||
// 计算编辑器区域的样式
|
||||
const editorAreaStyle = computed(() => {
|
||||
if (!editorVisible.value) return {}
|
||||
|
||||
// 优先使用像素高度,否则使用百分比
|
||||
if (editorAreaPixelHeight.value !== null) {
|
||||
return { height: `${editorAreaPixelHeight.value}px` }
|
||||
}
|
||||
return { height: `${editorAreaHeight.value}%` }
|
||||
})
|
||||
|
||||
// 更新编辑器区域的像素高度
|
||||
const updateEditorPixelHeight = () => {
|
||||
if (!mainLayoutRef.value || !editorVisible.value) {
|
||||
editorAreaPixelHeight.value = null
|
||||
return
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const mainLayoutEl = (mainLayoutRef.value as any)?.$el || mainLayoutRef.value
|
||||
if (mainLayoutEl instanceof HTMLElement) {
|
||||
const containerHeight = mainLayoutEl.getBoundingClientRect().height
|
||||
if (containerHeight > 0) {
|
||||
editorAreaPixelHeight.value = (containerHeight * editorAreaHeight.value) / 100
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听编辑器高度和可见性变化
|
||||
watch(() => editorAreaHeight.value, updateEditorPixelHeight)
|
||||
watch(() => editorVisible.value, (visible) => {
|
||||
if (visible) {
|
||||
updateEditorPixelHeight()
|
||||
} else {
|
||||
editorAreaPixelHeight.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const handleEditorResultDividerMouseDown = (e: MouseEvent) => {
|
||||
if ((e.target as HTMLElement).closest('.divider-toggle-btn')) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const mainLayoutEl = mainLayoutRef.value
|
||||
? ((mainLayoutRef.value as any)?.$el || mainLayoutRef.value)
|
||||
: (e.currentTarget as HTMLElement).closest('.main-layout')
|
||||
|
||||
if (!(mainLayoutEl instanceof HTMLElement)) return
|
||||
|
||||
const resizeHandler = createResizeHandler(mainLayoutEl, () => editorAreaHeight.value, {
|
||||
minPercent: 20,
|
||||
maxPercent: 80,
|
||||
minPixels: 150,
|
||||
onResize: (percentage) => {
|
||||
editorAreaHeight.value = percentage
|
||||
localStorage.setItem(STORAGE_KEYS.EDITOR_AREA_HEIGHT, String(percentage))
|
||||
|
||||
const containerHeight = mainLayoutEl.getBoundingClientRect().height
|
||||
if (containerHeight > 0) {
|
||||
editorAreaPixelHeight.value = (containerHeight * percentage) / 100
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
resizeHandler(e)
|
||||
}
|
||||
|
||||
// 导入事件类型
|
||||
import type {
|
||||
ConnectionSelectEvent,
|
||||
ConnectionEditEvent,
|
||||
ConnectionDeleteEvent,
|
||||
ConnectionTestEvent,
|
||||
ConnectionRefreshEvent,
|
||||
TableSelectEvent,
|
||||
TableStructureEvent
|
||||
} from './types/events'
|
||||
|
||||
// 恢复表结构状态(用于页面刷新或重新进入时的状态恢复)
|
||||
const restoreStructureState = async () => {
|
||||
const savedInfo = structureStore.restoreStructureInfo()
|
||||
if (!savedInfo?.tableName) return
|
||||
|
||||
// 检查连接是否匹配
|
||||
if (!currentConnection.value || currentConnection.value.id !== savedInfo.connectionId) return
|
||||
|
||||
// 避免重复加载
|
||||
if (structureStore.loading.value) return
|
||||
|
||||
// 如果当前已经有不同表的信息,不恢复
|
||||
const currentInfo = structureStore.info.value
|
||||
if (currentInfo?.tableName && currentInfo.tableName !== savedInfo.tableName) return
|
||||
|
||||
const currentTab = resultPanelRef.value?.getCurrentTab() || 'result'
|
||||
|
||||
// 如果当前不是结果Tab,需要切换到结构Tab
|
||||
if (currentTab !== 'result' && resultPanelRef.value) {
|
||||
(resultPanelRef.value as any).switchToStructureTab()
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
// 再次检查加载状态(切换Tab可能触发其他加载)
|
||||
if (structureStore.loading.value) return
|
||||
|
||||
// 如果当前是结果Tab,不加载结构(保持用户在结果Tab查看数据)
|
||||
if (currentTab === 'result') return
|
||||
|
||||
// 重新加载表结构
|
||||
await structureStore.loadStructure(
|
||||
savedInfo.connectionId,
|
||||
savedInfo.database,
|
||||
savedInfo.tableName,
|
||||
savedInfo.dbType,
|
||||
savedInfo.nodeType
|
||||
)
|
||||
}
|
||||
|
||||
// 连接选择
|
||||
const handleConnectionSelect = async (data: ConnectionSelectEvent) => {
|
||||
selectConnection(data.connection, data.database)
|
||||
clearResults()
|
||||
addMessage('info', `切换到连接: ${data.connection.name}${data.database ? ` (${data.database})` : ''}`)
|
||||
|
||||
// 连接切换后延迟恢复表结构状态(给 table-structure 事件处理时间)
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
await restoreStructureState()
|
||||
}
|
||||
|
||||
// 连接编辑
|
||||
const handleConnectionEdit = (data: ConnectionEditEvent) => {
|
||||
editConnection(data.connectionId)
|
||||
}
|
||||
|
||||
const handleConnectionDelete = async (data: ConnectionDeleteEvent) => {
|
||||
const isCurrent = deleteConnectionAction(data.connectionId)
|
||||
if (isCurrent) clearResults()
|
||||
await connectionTreeRef.value?.refresh?.()
|
||||
}
|
||||
|
||||
const handleNewConnection = () => newConnection()
|
||||
|
||||
const handleConnectionRefresh = async (data: ConnectionRefreshEvent) => {
|
||||
await connectionTreeRef.value?.refreshNode?.(data.connectionId, data.nodeType, data.database)
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
const handleConnectionTest = async (data: ConnectionTestEvent) => {
|
||||
try {
|
||||
await window.go?.main?.App?.TestDbConnection?.(data.connectionId)
|
||||
Message.success('连接测试成功')
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
Message.error('连接测试失败: ' + errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成数据库查询命令
|
||||
const generateQueryCommand = (dbType: string, database: string, tableName: string, pretty: boolean = false): string => {
|
||||
if (dbType === 'mongo') {
|
||||
const command = {
|
||||
op: "find",
|
||||
collection: tableName,
|
||||
filter: {},
|
||||
limit: 100
|
||||
}
|
||||
return pretty ? JSON.stringify(command, null, 2) : JSON.stringify(command)
|
||||
} else if (dbType === 'redis') {
|
||||
return `GET "${tableName}"`
|
||||
} else {
|
||||
return `SELECT * FROM \`${database}\`.\`${tableName}\` LIMIT 10;`
|
||||
}
|
||||
}
|
||||
|
||||
// 表选择(生成SQL/命令)
|
||||
const handleTableSelect = (data: TableSelectEvent) => {
|
||||
const dbType = data.dbType || currentConnection.value?.type || 'mysql'
|
||||
const sql = generateQueryCommand(dbType, data.database, data.tableName, true)
|
||||
sqlEditorRef.value?.insertSQL?.(sql)
|
||||
}
|
||||
|
||||
// 查询表数据(用于表节点点击时自动查询数据)
|
||||
const queryTableData = async (connectionId: number, database: string, tableName: string, dbType: 'mysql' | 'mongo' | 'redis' = 'mysql', nodeType: string = 'table') => {
|
||||
if (!currentConnection.value || currentConnection.value.id !== connectionId) return
|
||||
|
||||
// 保存表信息到 structureStore,以便切换到"结构"Tab时能自动加载
|
||||
structureStore.info.value = { connectionId, database, tableName, dbType, nodeType }
|
||||
|
||||
const sql = generateQueryCommand(dbType, database, tableName)
|
||||
await handleExecuteSQL(sql) // 用 handleExecuteSQL 保存原始 SQL,支持翻页
|
||||
}
|
||||
|
||||
const handleTableStructure = async (data: TableStructureEvent) => {
|
||||
if (!editorVisible.value) toggleEditor()
|
||||
|
||||
const currentTab = resultPanelRef.value?.getCurrentTab() || 'result'
|
||||
|
||||
if (currentTab === 'result') {
|
||||
await queryTableData(data.connectionId, data.database, data.tableName, data.dbType, data.nodeType)
|
||||
} else if (currentTab === 'structure') {
|
||||
const currentInfo = structureStore.info.value
|
||||
const isDifferentTable = !currentInfo ||
|
||||
currentInfo.connectionId !== data.connectionId ||
|
||||
currentInfo.database !== data.database ||
|
||||
currentInfo.tableName !== data.tableName
|
||||
|
||||
if (isDifferentTable && structureEditMode.value === 'edit') switchToViewMode()
|
||||
|
||||
await structureStore.loadStructure(
|
||||
data.connectionId,
|
||||
data.database,
|
||||
data.tableName,
|
||||
data.dbType,
|
||||
data.nodeType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 查看历史记录(将历史记录加载到结果面板显示)
|
||||
const handleViewHistory = (historyItem: any) => {
|
||||
if (!historyItem) return
|
||||
|
||||
// 根据历史记录类型设置结果数据
|
||||
if (historyItem.type === 'query') {
|
||||
resultState.setQueryResult(
|
||||
historyItem.data || [],
|
||||
{
|
||||
rowsAffected: historyItem.rows_affected || 0,
|
||||
executionTime: historyItem.execution_time || 0
|
||||
},
|
||||
historyItem.columns || []
|
||||
)
|
||||
} else if (historyItem.type === 'update') {
|
||||
resultState.setUpdateResult({
|
||||
rowsAffected: historyItem.rows_affected || 0,
|
||||
executionTime: historyItem.execution_time || 0
|
||||
})
|
||||
} else {
|
||||
resultState.setCommandResult(
|
||||
historyItem.data,
|
||||
{
|
||||
rowsAffected: historyItem.rows_affected || 0,
|
||||
executionTime: historyItem.execution_time || 0
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabChange = async (newTab: string, oldTab: string) => {
|
||||
const structureInfo = structureStore.info.value
|
||||
if (!structureInfo?.tableName) return
|
||||
if (!currentConnection.value || currentConnection.value.id !== structureInfo.connectionId) return
|
||||
|
||||
if (newTab === 'result' && oldTab !== 'result') {
|
||||
await queryTableData(
|
||||
structureInfo.connectionId,
|
||||
structureInfo.database,
|
||||
structureInfo.tableName,
|
||||
structureInfo.dbType
|
||||
)
|
||||
} else if (newTab === 'structure' && oldTab !== 'structure') {
|
||||
const currentData = structureStore.data.value
|
||||
if (!currentData || (currentData.type === 'mysql' && currentData.table !== structureInfo.tableName)) {
|
||||
await structureStore.loadStructure(
|
||||
structureInfo.connectionId,
|
||||
structureInfo.database,
|
||||
structureInfo.tableName,
|
||||
structureInfo.dbType,
|
||||
structureInfo.nodeType
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 开始创建表
|
||||
const handleCreateTable = (data: { connectionId: number; database: string; dbType: 'mysql' | 'mongo' | 'redis' }) => {
|
||||
// 如果结果面板隐藏,自动显示编辑器(这样结果面板也会显示)
|
||||
if (!editorVisible.value) {
|
||||
toggleEditor()
|
||||
}
|
||||
|
||||
startCreate(data.connectionId, data.database, data.dbType)
|
||||
}
|
||||
|
||||
// 取消创建
|
||||
const handleCancelCreate = () => {
|
||||
cancelCreate()
|
||||
}
|
||||
|
||||
// 提交创建表
|
||||
const handleCreateTableSubmit = async (data: { connectionId: number; database: string; tableName: string; sql: string }) => {
|
||||
try {
|
||||
createLoading.value = true
|
||||
|
||||
// 执行 CREATE TABLE SQL
|
||||
const result = await executeQuery(
|
||||
data.connectionId,
|
||||
data.sql,
|
||||
data.database
|
||||
)
|
||||
|
||||
Message.success(`表 ${data.tableName} 创建成功`)
|
||||
addMessage('success', `表 ${data.tableName} 创建成功`)
|
||||
|
||||
// 取消创建状态
|
||||
cancelCreate()
|
||||
|
||||
// 刷新连接树(刷新表列表)
|
||||
if (connectionTreeRef.value) {
|
||||
await connectionTreeRef.value.refresh()
|
||||
}
|
||||
|
||||
// 切换到结构 Tab 并加载新创建的表结构
|
||||
if (resultPanelRef.value) {
|
||||
(resultPanelRef.value as any).switchToStructureTab()
|
||||
}
|
||||
|
||||
// 等待一下确保Tab切换完成
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 加载新创建的表结构
|
||||
await structureStore.loadStructure(
|
||||
data.connectionId,
|
||||
data.database,
|
||||
data.tableName,
|
||||
'mysql',
|
||||
'table'
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
Message.error('创建表失败: ' + errorMessage)
|
||||
addMessage('error', '创建表失败: ' + errorMessage)
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存当前执行的 SQL(用于分页)
|
||||
const currentExecutedSQL = ref('')
|
||||
|
||||
// 执行SQL
|
||||
const handleExecuteSQL = async (sql: string, page?: number, pageSize?: number) => {
|
||||
// 保存原始 SQL(不包含分页信息)
|
||||
if (page == null && pageSize == null) {
|
||||
currentExecutedSQL.value = sql
|
||||
}
|
||||
|
||||
const resolvedPage = page ?? 1
|
||||
const resolvedPageSize = pageSize ?? 10
|
||||
await executeSQL(sql, currentConnection.value, selectedDatabase.value, resolvedPage, resolvedPageSize)
|
||||
// 执行完成后,等待一下确保结果已经设置,然后切换到结果 tab
|
||||
await nextTick()
|
||||
setTimeout(() => {
|
||||
if (resultPanelRef.value && (resultData.value !== null || resultStats.value !== null)) {
|
||||
resultPanelRef.value.switchToResultTab()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 处理分页重新执行 SQL
|
||||
const handleReExecuteSQL = async (pagination: { page: number; pageSize: number }) => {
|
||||
if (!currentExecutedSQL.value) {
|
||||
Message.warning('无法翻页:缺少原始 SQL 语句')
|
||||
return
|
||||
}
|
||||
await handleExecuteSQL(currentExecutedSQL.value, pagination.page, pagination.pageSize)
|
||||
}
|
||||
|
||||
// 连接表单成功回调
|
||||
const handleConnectionSuccess = async () => {
|
||||
const editedId = editingConnectionId.value
|
||||
// 刷新连接列表
|
||||
if (connectionTreeRef.value) {
|
||||
await connectionTreeRef.value?.refresh()
|
||||
}
|
||||
onConnectionSuccess(editedId)
|
||||
}
|
||||
|
||||
// 表结构编辑相关处理
|
||||
const handleSwitchToEditMode = () => {
|
||||
const data = structureStore.data.value
|
||||
const info = structureStore.info.value
|
||||
if (!data || !info) {
|
||||
console.warn('切换到编辑模式失败:缺少数据或信息', { data, info })
|
||||
return
|
||||
}
|
||||
if (info.dbType === 'mysql' && (data.type === 'mysql' || !data.type)) {
|
||||
const columns = data.columns || []
|
||||
const indexes = data.indexes || []
|
||||
if (columns.length === 0) {
|
||||
console.warn('切换到编辑模式失败:字段列表为空', data)
|
||||
return
|
||||
}
|
||||
switchToEditMode(columns, indexes)
|
||||
} else if (info.dbType === 'mongo' && data.type === 'mongo') {
|
||||
switchToEditMode([], data.structure?.indexes || [])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSwitchToViewMode = () => {
|
||||
switchToViewMode()
|
||||
}
|
||||
|
||||
// 表结构保存处理(包含预览和用户确认流程)
|
||||
const handleSaveStructure = async () => {
|
||||
const info = structureStore.info.value
|
||||
if (!info) return
|
||||
|
||||
try {
|
||||
// 第一步:预览生成 SQL 语句
|
||||
const previewStatements = await previewTableStructure(
|
||||
info.connectionId,
|
||||
info.database,
|
||||
info.tableName,
|
||||
info.dbType
|
||||
)
|
||||
|
||||
// 如果没有变更,直接返回
|
||||
if (previewStatements.length === 0) {
|
||||
Message.info('表结构未发生变化')
|
||||
return
|
||||
}
|
||||
|
||||
// 第二步:显示确认对话框,让用户确认执行
|
||||
sqlPreviewStatements.value = previewStatements
|
||||
sqlPreviewDbType.value = info.dbType
|
||||
sqlPreviewInfo.value = {
|
||||
connectionId: info.connectionId,
|
||||
database: info.database,
|
||||
tableName: info.tableName,
|
||||
dbType: info.dbType
|
||||
}
|
||||
showSqlPreviewModal.value = true
|
||||
} catch (error: unknown) {
|
||||
console.error('预览表结构变更失败:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
Message.error('预览表结构变更失败: ' + errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
switchToViewMode()
|
||||
}
|
||||
|
||||
// 确认执行 SQL
|
||||
const handleConfirmSqlExecute = async () => {
|
||||
if (!sqlPreviewInfo.value) return
|
||||
|
||||
const info = sqlPreviewInfo.value
|
||||
showSqlPreviewModal.value = false
|
||||
|
||||
if (info.dbType === 'redis') {
|
||||
Message.error('Redis 不支持表结构修改')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await saveStructureEdit(
|
||||
info.connectionId,
|
||||
info.database,
|
||||
info.tableName,
|
||||
info.dbType as 'mysql' | 'mongo'
|
||||
)
|
||||
|
||||
if (result && result.success) {
|
||||
// 保存成功后刷新结构数据
|
||||
await structureStore.refreshStructure()
|
||||
|
||||
// 在消息面板中展示生成的 SQL 语句
|
||||
if (result.sqlStatements && result.sqlStatements.length > 0) {
|
||||
addMessage('success', `表结构变更成功,执行了 ${result.sqlStatements.length} 条语句`)
|
||||
// 为每条 SQL 语句添加消息
|
||||
result.sqlStatements.forEach((sql: string, index: number) => {
|
||||
addMessage('info', `[${index + 1}] ${sql}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新编辑数据
|
||||
const handleUpdateColumns = (columns: any[]) => {
|
||||
editedColumns.value = columns
|
||||
}
|
||||
|
||||
const handleUpdateIndexes = (indexes: any[]) => {
|
||||
editedIndexes.value = indexes
|
||||
}
|
||||
|
||||
// 添加字段
|
||||
const handleAddColumn = () => {
|
||||
addColumn()
|
||||
}
|
||||
|
||||
// 清理本地缓存
|
||||
const handleClearCache = () => {
|
||||
Modal.confirm({
|
||||
title: '清理本地缓存',
|
||||
content: '确定要清理所有本地缓存数据吗?这将清除编辑器状态、连接状态、展开状态等所有缓存信息。',
|
||||
onOk: () => {
|
||||
try {
|
||||
// 清理所有 localStorage 缓存
|
||||
Object.values(STORAGE_KEYS).forEach(key => {
|
||||
localStorage.removeItem(key)
|
||||
})
|
||||
Message.success('本地缓存已清理')
|
||||
|
||||
// 重置连接树状态
|
||||
if (connectionTreeRef.value) {
|
||||
connectionTreeRef.value.refresh()
|
||||
}
|
||||
|
||||
// 重置编辑器状态
|
||||
clearResults()
|
||||
} catch (error) {
|
||||
Message.error('清理缓存失败: ' + (error.message || error))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听容器大小变化,更新编辑器区域高度
|
||||
let mainLayoutResizeObserver: ResizeObserver | null = null
|
||||
|
||||
// 组件挂载时的初始化工作
|
||||
onMounted(async () => {
|
||||
// 监听 Wails 事件(来自窗口菜单的清理缓存功能)
|
||||
if (window.runtime?.EventsOn) {
|
||||
window.runtime.EventsOn('clear-cache', () => {
|
||||
handleClearCache()
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化编辑器像素高度并监听容器大小变化
|
||||
nextTick(() => {
|
||||
updateEditorPixelHeight()
|
||||
|
||||
const mainLayoutEl = mainLayoutRef.value
|
||||
? ((mainLayoutRef.value as any)?.$el || mainLayoutRef.value)
|
||||
: null
|
||||
|
||||
if (mainLayoutEl instanceof HTMLElement) {
|
||||
mainLayoutResizeObserver = new ResizeObserver(updateEditorPixelHeight)
|
||||
mainLayoutResizeObserver.observe(mainLayoutEl)
|
||||
}
|
||||
})
|
||||
|
||||
// 加载保存的标签页内容
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
if (sqlEditorRef.value?.loadSavedTabs) {
|
||||
try {
|
||||
await sqlEditorRef.value.loadSavedTabs()
|
||||
} catch (error) {
|
||||
console.warn('加载保存的标签页失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时的清理工作
|
||||
onUnmounted(() => {
|
||||
// 取消 Wails 事件监听
|
||||
if (window.runtime?.EventsOff) {
|
||||
window.runtime.EventsOff('clear-cache')
|
||||
}
|
||||
|
||||
// 清理 ResizeObserver 避免内存泄漏
|
||||
if (mainLayoutResizeObserver) {
|
||||
mainLayoutResizeObserver.disconnect()
|
||||
mainLayoutResizeObserver = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 主布局容器 */
|
||||
.db-cli-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 侧边栏 - 使用 Arco 设计令牌 */
|
||||
.sidebar {
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
border-right: 1px solid var(--color-border-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 主布局容器 - 使用 Arco Layout */
|
||||
.main-layout {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 编辑器区域 - 使用 Arco Layout Content */
|
||||
.editor-area {
|
||||
flex: 0 0 auto !important; /* 覆盖 Arco 的 flex: auto,使用固定高度 */
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-area :deep(.sql-editor-wrapper) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 编辑器/结果分隔条 - 使用 Arco 设计令牌 */
|
||||
.editor-result-divider {
|
||||
flex-shrink: 0;
|
||||
height: 4px;
|
||||
background: var(--color-border-2);
|
||||
cursor: row-resize;
|
||||
position: relative;
|
||||
transition: background-color var(--transition-duration-2) var(--transition-timing-function-ease-out);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.editor-result-divider:hover {
|
||||
background: var(--color-border-3);
|
||||
}
|
||||
|
||||
.editor-result-divider.collapsed {
|
||||
cursor: pointer;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.editor-result-divider.collapsed:hover {
|
||||
background: var(--color-primary-light-4);
|
||||
}
|
||||
|
||||
.divider-toggle-btn {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: var(--color-bg-1);
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: var(--border-radius-small);
|
||||
box-shadow: var(--shadow-1-down);
|
||||
transition: all var(--transition-duration-2) var(--transition-timing-function-ease-out);
|
||||
padding: 0;
|
||||
min-width: 30px;
|
||||
height: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.divider-toggle-btn:hover {
|
||||
background: var(--color-bg-2);
|
||||
border-color: var(--color-primary-light-2);
|
||||
box-shadow: var(--shadow-2-down);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.divider-toggle-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: var(--shadow-1-down);
|
||||
}
|
||||
|
||||
/* 结果区域 - 使用 Arco Layout Content */
|
||||
.result-area {
|
||||
flex: 1;
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--color-border-2);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.result-area :deep(.result-panel-wrapper) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user