Private
Public Access
1
0

新增:连接管理、数据查询等功能

This commit is contained in:
2026-01-22 18:34:59 +08:00
parent 95d3a20292
commit 652f5e5d60
87 changed files with 15082 additions and 162 deletions

View 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>