- 简化计算属性,删除重复代码 - 优化文件扩展名获取逻辑 - 新增文件工具函数库 fileHelpers.js - 增强 CodeEditor 语法高亮(支持 30+ 语言) - 修复 Office 文档文件服务器访问权限 - 添加特殊文件名支持(Dockerfile、Makefile 等)
972 lines
29 KiB
Vue
972 lines
29 KiB
Vue
<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">
|
||
// 定义组件名称,用于 KeepAlive 缓存
|
||
defineOptions({
|
||
name: 'DbCli'
|
||
})
|
||
|
||
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>
|