fdb2 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +21 -0
- package/.editorconfig +11 -0
- package/.eslintrc.cjs +14 -0
- package/.eslintrc.json +7 -0
- package/.prettierrc.js +3 -0
- package/.tpl.env +22 -0
- package/README.md +260 -0
- package/bin/build.sh +28 -0
- package/bin/deploy.sh +8 -0
- package/bin/dev.sh +10 -0
- package/bin/docker/.env +4 -0
- package/bin/docker/dev-docker-compose.yml +43 -0
- package/bin/docker/dev.Dockerfile +24 -0
- package/bin/docker/prod-docker-compose.yml +17 -0
- package/bin/docker/prod.Dockerfile +29 -0
- package/bin/fdb2.js +142 -0
- package/data/connections.demo.json +32 -0
- package/env.d.ts +1 -0
- package/nw-build.js +120 -0
- package/nw-dev.js +65 -0
- package/package.json +114 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +9 -0
- package/public/modules/header.tpl +14 -0
- package/public/modules/initial_state.tpl +55 -0
- package/server/index.ts +677 -0
- package/server/model/connection.entity.ts +66 -0
- package/server/model/database.entity.ts +246 -0
- package/server/service/connection.service.ts +334 -0
- package/server/service/database/base.service.ts +363 -0
- package/server/service/database/database.service.ts +510 -0
- package/server/service/database/index.ts +7 -0
- package/server/service/database/mssql.service.ts +723 -0
- package/server/service/database/mysql.service.ts +761 -0
- package/server/service/database/oracle.service.ts +839 -0
- package/server/service/database/postgres.service.ts +744 -0
- package/server/service/database/sqlite.service.ts +559 -0
- package/server/service/session.service.ts +158 -0
- package/server.js +128 -0
- package/src/adapter/ajax.ts +135 -0
- package/src/assets/base.css +1 -0
- package/src/assets/database.css +950 -0
- package/src/assets/images/collapse.png +0 -0
- package/src/assets/images/no-login.png +0 -0
- package/src/assets/images/svg/illustrations/illustration-1.svg +1 -0
- package/src/assets/images/svg/illustrations/illustration-2.svg +2 -0
- package/src/assets/images/svg/illustrations/illustration-3.svg +50 -0
- package/src/assets/images/svg/illustrations/illustration-4.svg +1 -0
- package/src/assets/images/svg/illustrations/illustration-5.svg +73 -0
- package/src/assets/images/svg/illustrations/illustration-6.svg +89 -0
- package/src/assets/images/svg/illustrations/illustration-7.svg +39 -0
- package/src/assets/images/svg/illustrations/illustration-8.svg +1 -0
- package/src/assets/images/svg/separators/curve-2.svg +3 -0
- package/src/assets/images/svg/separators/curve.svg +3 -0
- package/src/assets/images/svg/separators/line.svg +3 -0
- package/src/assets/images/theme/light/screen-1-1000x800.jpg +0 -0
- package/src/assets/images/theme/light/screen-2-1000x800.jpg +0 -0
- package/src/assets/login/bg.jpg +0 -0
- package/src/assets/login/bg.png +0 -0
- package/src/assets/login/left.jpg +0 -0
- package/src/assets/logo.svg +73 -0
- package/src/assets/logo.webp +0 -0
- package/src/assets/main.css +1 -0
- package/src/base/config.ts +20 -0
- package/src/base/detect.ts +134 -0
- package/src/base/entity.ts +92 -0
- package/src/base/eventBus.ts +37 -0
- package/src/base//345/237/272/347/241/200/345/261/202.md +7 -0
- package/src/components/connection-editor/index.vue +590 -0
- package/src/components/dataGrid/index.vue +105 -0
- package/src/components/dataGrid/pagination.vue +106 -0
- package/src/components/loading/index.vue +43 -0
- package/src/components/modal/index.ts +181 -0
- package/src/components/modal/index.vue +560 -0
- package/src/components/toast/index.ts +44 -0
- package/src/components/toast/toast.vue +58 -0
- package/src/components/user/name.vue +104 -0
- package/src/components/user/selector.vue +416 -0
- package/src/domain/SysConfig.ts +74 -0
- package/src/platform/App.vue +8 -0
- package/src/platform/database/components/connection-detail.vue +1154 -0
- package/src/platform/database/components/data-editor.vue +478 -0
- package/src/platform/database/components/data-import-export.vue +1602 -0
- package/src/platform/database/components/database-detail.vue +1173 -0
- package/src/platform/database/components/database-monitor.vue +1086 -0
- package/src/platform/database/components/db-tools.vue +577 -0
- package/src/platform/database/components/query-history.vue +1349 -0
- package/src/platform/database/components/sql-executor.vue +738 -0
- package/src/platform/database/components/sql-query-editor.vue +1046 -0
- package/src/platform/database/components/table-detail.vue +1376 -0
- package/src/platform/database/components/table-editor.vue +690 -0
- package/src/platform/database/explorer.vue +1840 -0
- package/src/platform/database/index.vue +1193 -0
- package/src/platform/database/layout.vue +367 -0
- package/src/platform/database/router.ts +37 -0
- package/src/platform/database/styles/common.scss +602 -0
- package/src/platform/database/types/common.ts +445 -0
- package/src/platform/database/utils/export.ts +232 -0
- package/src/platform/database/utils/helpers.ts +437 -0
- package/src/platform/index.ts +33 -0
- package/src/platform/router.ts +41 -0
- package/src/service/base.ts +128 -0
- package/src/service/database.ts +500 -0
- package/src/service/login.ts +121 -0
- package/src/shims-vue.d.ts +7 -0
- package/src/stores/connection.ts +266 -0
- package/src/stores/session.ts +87 -0
- package/src/typings/database-types.ts +413 -0
- package/src/typings/database.ts +364 -0
- package/src/typings/global.d.ts +58 -0
- package/src/typings/pinia.d.ts +8 -0
- package/src/utils/clipboard.ts +30 -0
- package/src/utils/database-types.ts +243 -0
- package/src/utils/modal.ts +124 -0
- package/src/utils/request.ts +55 -0
- package/src/utils/sleep.ts +4 -0
- package/src/utils/toast.ts +73 -0
- package/src/utils/util.ts +171 -0
- package/src/utils/xlsx.ts +228 -0
- package/tsconfig.json +33 -0
- package/tsconfig.server.json +19 -0
- package/view/index.html +9 -0
- package/view/modules/header.tpl +14 -0
- package/view/modules/initial_state.tpl +20 -0
- package/vite.config.ts +384 -0
|
@@ -0,0 +1,1840 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="database-explorer">
|
|
3
|
+
<div class="explorer-layout">
|
|
4
|
+
<!-- 左侧树形菜单 -->
|
|
5
|
+
<div class="explorer-sidebar">
|
|
6
|
+
<div class="sidebar-header">
|
|
7
|
+
<h5 class="sidebar-title">
|
|
8
|
+
<i class="bi bi-diagram-3"></i>
|
|
9
|
+
数据库浏览器
|
|
10
|
+
</h5>
|
|
11
|
+
<div class="sidebar-actions">
|
|
12
|
+
<button class="btn btn-sm btn-outline-primary" @click="refreshAll" title="刷新">
|
|
13
|
+
<i class="bi bi-arrow-clockwise"></i>
|
|
14
|
+
</button>
|
|
15
|
+
<button class="btn btn-sm btn-outline-success" @click="showAddConnectionModal" title="添加连接">
|
|
16
|
+
<i class="bi bi-plus"></i>
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="sidebar-content">
|
|
22
|
+
<!-- 连接树 -->
|
|
23
|
+
<div class="tree-container">
|
|
24
|
+
<div
|
|
25
|
+
v-for="connection in connections"
|
|
26
|
+
:key="connection.id"
|
|
27
|
+
class="tree-node connection-node"
|
|
28
|
+
:class="{ 'selected': selectedConnection?.id === connection.id && !selectedDatabase && !selectedTable }"
|
|
29
|
+
>
|
|
30
|
+
<!-- 连接节点 -->
|
|
31
|
+
<div class="node-content connection-content">
|
|
32
|
+
<div class="node-expand" @click="toggleConnection(connection)">
|
|
33
|
+
<i class="bi bi-chevron-right" :class="{ 'expanded': expandedConnections.has(connection.id) }"></i>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="node-main" @click="selectConnection(connection)">
|
|
36
|
+
<div class="node-icon">
|
|
37
|
+
<div class="db-logo" :class="getDbLogoClass(connection.type)">
|
|
38
|
+
{{ getDbLogoText(connection.type) }}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="node-label">
|
|
42
|
+
<span class="connection-name">{{ connection.name }}</span>
|
|
43
|
+
<span class="connection-type">{{ getDbTypeLabel(connection.type) }}</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="node-actions">
|
|
47
|
+
<button
|
|
48
|
+
class="btn btn-sm btn-icon"
|
|
49
|
+
@click.stop="refreshConnection(connection)"
|
|
50
|
+
title="刷新连接"
|
|
51
|
+
>
|
|
52
|
+
<i class="bi bi-arrow-clockwise"></i>
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
class="btn btn-sm btn-icon"
|
|
56
|
+
@click.stop="editConnection(connection)"
|
|
57
|
+
title="编辑连接"
|
|
58
|
+
>
|
|
59
|
+
<i class="bi bi-pencil"></i>
|
|
60
|
+
</button>
|
|
61
|
+
<button
|
|
62
|
+
class="btn btn-sm btn-icon"
|
|
63
|
+
@click.stop="testConnection(connection)"
|
|
64
|
+
title="测试连接"
|
|
65
|
+
>
|
|
66
|
+
<i class="bi bi-wifi"></i>
|
|
67
|
+
</button>
|
|
68
|
+
<button
|
|
69
|
+
class="btn btn-sm btn-icon btn-icon-danger"
|
|
70
|
+
@click.stop="deleteConnection(connection)"
|
|
71
|
+
title="删除连接"
|
|
72
|
+
>
|
|
73
|
+
<i class="bi bi-trash"></i>
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="node-spinner" v-if="loadingConnections.has(connection.id)">
|
|
77
|
+
<div class="spinner-border spinner-border-sm"></div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- 数据库子节点 -->
|
|
82
|
+
<div v-if="expandedConnections.has(connection.id)" class="tree-children">
|
|
83
|
+
<div
|
|
84
|
+
v-for="database in getDatabasesForConnection(connection.id)"
|
|
85
|
+
:key="`${connection.id}-${database}`"
|
|
86
|
+
class="tree-node database-node"
|
|
87
|
+
:class="{ 'selected': selectedDatabase === database && selectedConnection?.id === connection.id && !selectedTable }"
|
|
88
|
+
>
|
|
89
|
+
<!-- 数据库节点 -->
|
|
90
|
+
<div class="node-content database-content">
|
|
91
|
+
<div class="node-expand" @click="toggleDatabase(connection, database)">
|
|
92
|
+
<i class="bi bi-chevron-right" :class="{ 'expanded': expandedDatabases.has(`${connection.id}-${database}`) }"></i>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="node-main" @click="selectDatabase(connection, database)">
|
|
95
|
+
<div class="node-icon">
|
|
96
|
+
<i class="bi bi-database"></i>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="node-label">
|
|
99
|
+
<span class="database-name">{{ database }}</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="node-actions">
|
|
103
|
+
<button
|
|
104
|
+
class="btn btn-sm btn-icon"
|
|
105
|
+
@click.stop="refreshDatabase(connection, database)"
|
|
106
|
+
title="刷新数据库"
|
|
107
|
+
>
|
|
108
|
+
<i class="bi bi-arrow-clockwise"></i>
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
class="btn btn-sm btn-icon btn-icon-danger"
|
|
112
|
+
@click.stop="deleteDatabase(connection, database)"
|
|
113
|
+
title="删除数据库"
|
|
114
|
+
>
|
|
115
|
+
<i class="bi bi-trash"></i>
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="node-spinner" v-if="loadingDatabases.has(`${connection.id}-${database}`)">
|
|
119
|
+
<div class="spinner-border spinner-border-sm"></div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- 表子节点 -->
|
|
124
|
+
<div v-if="expandedDatabases.has(`${connection.id}-${database}`)" class="tree-children">
|
|
125
|
+
<div
|
|
126
|
+
v-for="table in getTablesForDatabase(connection.id, database)"
|
|
127
|
+
:key="`${connection.id}-${database}-${table.name}`"
|
|
128
|
+
class="tree-node table-node"
|
|
129
|
+
:class="{ 'selected': selectedTable?.name === table.name && selectedDatabase === database && selectedConnection?.id === connection.id }"
|
|
130
|
+
>
|
|
131
|
+
<!-- 表节点 -->
|
|
132
|
+
<div class="node-content table-content">
|
|
133
|
+
<div class="node-icon">
|
|
134
|
+
<i class="bi bi-table"></i>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="node-main" @click="selectTable(connection, database, table)">
|
|
137
|
+
<div class="node-label">
|
|
138
|
+
<span class="table-name">{{ table.name }}</span>
|
|
139
|
+
<span class="table-info" v-if="table.rowCount !== undefined">{{ formatNumber(table.rowCount) }} 行</span>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="node-actions">
|
|
143
|
+
<button
|
|
144
|
+
class="btn btn-sm btn-icon"
|
|
145
|
+
@click.stop="refreshTable(connection, database, table)"
|
|
146
|
+
title="刷新表"
|
|
147
|
+
>
|
|
148
|
+
<i class="bi bi-arrow-clockwise"></i>
|
|
149
|
+
</button>
|
|
150
|
+
<button
|
|
151
|
+
class="btn btn-sm btn-icon"
|
|
152
|
+
@click.stop="viewTableStructure(connection, database, table)"
|
|
153
|
+
title="查看结构"
|
|
154
|
+
>
|
|
155
|
+
<i class="bi bi-diagram-3"></i>
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- 空状态 -->
|
|
167
|
+
<div v-if="connections.length === 0" class="empty-state">
|
|
168
|
+
<div class="empty-icon">
|
|
169
|
+
<i class="bi bi-inbox"></i>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="empty-text">
|
|
172
|
+
<p>还没有数据库连接</p>
|
|
173
|
+
<button class="btn btn-primary btn-sm" @click="showAddConnectionModal">
|
|
174
|
+
<i class="bi bi-plus"></i> 添加连接
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- 右侧内容区域 -->
|
|
182
|
+
<div class="explorer-main">
|
|
183
|
+
<!-- 连接详情组件 -->
|
|
184
|
+
<ConnectionDetail
|
|
185
|
+
v-if="selectedConnection && !selectedDatabase && !selectedTable"
|
|
186
|
+
:connection="selectedConnection"
|
|
187
|
+
@test-connection="handleTestConnection"
|
|
188
|
+
@edit-connection="handleEditConnection"
|
|
189
|
+
@refresh-all="handleRefreshAll"
|
|
190
|
+
@open-sql-query="handleOpenSqlQuery"
|
|
191
|
+
@export-schema="handleExportSchema"
|
|
192
|
+
@view-logs="handleViewLogs"
|
|
193
|
+
@create-database="handleCreateDatabase"
|
|
194
|
+
/>
|
|
195
|
+
|
|
196
|
+
<!-- 数据库详情组件 -->
|
|
197
|
+
<DatabaseDetail
|
|
198
|
+
v-else-if="selectedConnection && selectedDatabase && !selectedTable"
|
|
199
|
+
:connection="selectedConnection"
|
|
200
|
+
:database="selectedDatabase"
|
|
201
|
+
:tables="getTablesForDatabase(selectedConnection.id||'', selectedDatabase)"
|
|
202
|
+
:database-info="databaseInfo"
|
|
203
|
+
:loading="loadingDatabases.has(`${selectedConnection.id}-${selectedDatabase}`)"
|
|
204
|
+
@select-table="selectTable"
|
|
205
|
+
@refresh-database="handleRefreshDatabase"
|
|
206
|
+
@create-table="handleCreateTable"
|
|
207
|
+
@execute-sql="handleExecuteSql"
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
<!-- 表详情组件 -->
|
|
211
|
+
<TableDetail
|
|
212
|
+
v-else-if="selectedConnection && selectedDatabase && selectedTable"
|
|
213
|
+
:connection="selectedConnection"
|
|
214
|
+
:database="selectedDatabase"
|
|
215
|
+
:table="selectedTable"
|
|
216
|
+
:table-data="tableData"
|
|
217
|
+
:table-structure="tableStructure"
|
|
218
|
+
:loading="isGlobalLoading"
|
|
219
|
+
:total="totalRecords"
|
|
220
|
+
:sql-result="sqlResult"
|
|
221
|
+
:sql-executing="sqlExecuting"
|
|
222
|
+
@refresh-data="refreshTableData"
|
|
223
|
+
@refresh-database="handleRefreshDatabase"
|
|
224
|
+
@refresh-structure="handleRefreshStructure"
|
|
225
|
+
@insert-data="handleInsertData"
|
|
226
|
+
@export-table="handleExportTable"
|
|
227
|
+
@truncate-table="handleTruncateTable"
|
|
228
|
+
@edit-row="handleEditRow"
|
|
229
|
+
@delete-row="handleDeleteRow"
|
|
230
|
+
@execute-sql="handleExecuteSql"
|
|
231
|
+
/>
|
|
232
|
+
|
|
233
|
+
<!-- 默认空状态 -->
|
|
234
|
+
<div v-else class="default-state">
|
|
235
|
+
<div class="default-content">
|
|
236
|
+
<div class="default-icon">
|
|
237
|
+
<i class="bi bi-diagram-3"></i>
|
|
238
|
+
</div>
|
|
239
|
+
<h5>数据库浏览器</h5>
|
|
240
|
+
<p>请从左侧选择一个连接、数据库或表来查看详细信息</p>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<!-- 全局Loading -->
|
|
247
|
+
<!-- <Loading :isLoading="isGlobalLoading" :message="loadingMessage" /> -->
|
|
248
|
+
|
|
249
|
+
<!-- 连接编辑器 -->
|
|
250
|
+
<ConnectionEditor ref="connectionEditorRef" @saved="onConnectionSaved" />
|
|
251
|
+
|
|
252
|
+
<!-- Toast -->
|
|
253
|
+
<Toast ref="toastRef" />
|
|
254
|
+
</div>
|
|
255
|
+
</template>
|
|
256
|
+
|
|
257
|
+
<script lang="ts" setup>
|
|
258
|
+
import { ref, onMounted, computed, watch } from 'vue';
|
|
259
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
260
|
+
import { ConnectionService, DatabaseService } from '@/service/database';
|
|
261
|
+
import type { ConnectionEntity, TableEntity } from '@/typings/database';
|
|
262
|
+
import ConnectionEditor from '@/components/connection-editor/index.vue';
|
|
263
|
+
import Toast from '@/components/toast/toast.vue';
|
|
264
|
+
import Loading from '@/components/loading/index.vue';
|
|
265
|
+
import ConnectionDetail from './components/connection-detail.vue';
|
|
266
|
+
import DatabaseDetail from './components/database-detail.vue';
|
|
267
|
+
import TableDetail from './components/table-detail.vue';
|
|
268
|
+
import { modal } from '@/utils/modal';
|
|
269
|
+
|
|
270
|
+
const route = useRoute();
|
|
271
|
+
const router = useRouter();
|
|
272
|
+
const connectionService = new ConnectionService();
|
|
273
|
+
const databaseService = new DatabaseService();
|
|
274
|
+
|
|
275
|
+
// 响应式数据
|
|
276
|
+
const connections = ref<ConnectionEntity[]>([]);
|
|
277
|
+
const selectedConnection = ref<ConnectionEntity | null>(null);
|
|
278
|
+
const selectedDatabase = ref<string>('');
|
|
279
|
+
const selectedTable = ref<TableEntity | null>(null);
|
|
280
|
+
|
|
281
|
+
// 树形展开状态
|
|
282
|
+
const expandedConnections = ref(new Set<string>());
|
|
283
|
+
const expandedDatabases = ref(new Set<string>());
|
|
284
|
+
|
|
285
|
+
// 加载状态
|
|
286
|
+
const loadingDatabases = ref(new Set<string>());
|
|
287
|
+
const loadingTables = ref(new Set<string>());
|
|
288
|
+
const loadingConnections = ref(new Set<string>());
|
|
289
|
+
|
|
290
|
+
// 数据缓存
|
|
291
|
+
const databaseCache = ref<Map<string, string[]>>(new Map());
|
|
292
|
+
const tableCache = ref<Map<string, TableEntity[]>>(new Map());
|
|
293
|
+
const databaseInfoCache = ref<Map<string, any>>(new Map());
|
|
294
|
+
|
|
295
|
+
// 标签页状态
|
|
296
|
+
const activeTab = ref('overview');
|
|
297
|
+
const activeTableTab = ref('data');
|
|
298
|
+
|
|
299
|
+
// 表数据相关
|
|
300
|
+
const tableData = ref<any[]>([]);
|
|
301
|
+
const tableStructure = ref<any>(null);
|
|
302
|
+
const tableColumns = ref<any[]>([]);
|
|
303
|
+
const tableDataSearch = ref('');
|
|
304
|
+
const currentPage = ref(1);
|
|
305
|
+
const pageSize = ref(50);
|
|
306
|
+
const totalRecords = ref(0);
|
|
307
|
+
|
|
308
|
+
// SQL执行结果
|
|
309
|
+
const sqlResult = ref<any>(null);
|
|
310
|
+
const sqlExecuting = ref(false);
|
|
311
|
+
|
|
312
|
+
// 全局Loading状态
|
|
313
|
+
const isGlobalLoading = ref(false);
|
|
314
|
+
const loadingMessage = ref('加载中...');
|
|
315
|
+
|
|
316
|
+
// 组件引用
|
|
317
|
+
const connectionEditorRef = ref();
|
|
318
|
+
const toastRef = ref();
|
|
319
|
+
|
|
320
|
+
// 计算属性
|
|
321
|
+
const databaseInfo = computed(() => {
|
|
322
|
+
if (!selectedConnection.value || !selectedDatabase.value) return null;
|
|
323
|
+
return databaseInfoCache.value.get(`${selectedConnection.value.id}-${selectedDatabase.value}`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const paginatedTableData = computed(() => {
|
|
327
|
+
const start = (currentPage.value - 1) * pageSize.value;
|
|
328
|
+
const end = start + pageSize.value;
|
|
329
|
+
return tableData.value.slice(start, end);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const totalPages = computed(() => {
|
|
333
|
+
return Math.ceil(tableData.value.length / pageSize.value);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// 生命周期
|
|
337
|
+
onMounted(() => {
|
|
338
|
+
loadConnections().then(() => {
|
|
339
|
+
// 加载连接后处理 URL query 参数
|
|
340
|
+
handleRouteQuery();
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 标志位,用于区分是用户操作还是 URL 变化
|
|
345
|
+
let isUpdatingFromUrl = false;
|
|
346
|
+
|
|
347
|
+
// 处理 URL query 参数
|
|
348
|
+
function handleRouteQuery() {
|
|
349
|
+
const connectionId = route.query.connectionId as string;
|
|
350
|
+
const database = route.query.database as string;
|
|
351
|
+
|
|
352
|
+
if (connectionId) {
|
|
353
|
+
// 查找对应的连接
|
|
354
|
+
const connection = connections.value.find(conn => conn.id === connectionId);
|
|
355
|
+
if (connection) {
|
|
356
|
+
// 设置标志位,避免触发 URL 更新
|
|
357
|
+
isUpdatingFromUrl = true;
|
|
358
|
+
|
|
359
|
+
// 选择连接
|
|
360
|
+
selectedConnection.value = connection;
|
|
361
|
+
selectedDatabase.value = '';
|
|
362
|
+
selectedTable.value = null;
|
|
363
|
+
activeTab.value = 'overview';
|
|
364
|
+
|
|
365
|
+
// 展开连接节点并加载数据库
|
|
366
|
+
if (!expandedConnections.value.has(connectionId)) {
|
|
367
|
+
expandedConnections.value.add(connectionId);
|
|
368
|
+
loadDatabasesForConnection(connection);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 如果提供了数据库参数,选择数据库
|
|
372
|
+
if (database) {
|
|
373
|
+
setTimeout(() => {
|
|
374
|
+
selectedDatabase.value = database;
|
|
375
|
+
selectedTable.value = null;
|
|
376
|
+
activeTab.value = 'tables';
|
|
377
|
+
|
|
378
|
+
// 展开数据库节点
|
|
379
|
+
const dbKey = `${connectionId}-${database}`;
|
|
380
|
+
if (!expandedDatabases.value.has(dbKey)) {
|
|
381
|
+
expandedDatabases.value.add(dbKey);
|
|
382
|
+
loadDatabaseInfo(connection, database);
|
|
383
|
+
loadTablesForDatabase(connection, database);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 重置标志位
|
|
387
|
+
isUpdatingFromUrl = false;
|
|
388
|
+
}, 100);
|
|
389
|
+
} else {
|
|
390
|
+
// 重置标志位
|
|
391
|
+
isUpdatingFromUrl = false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 监听 route.query 变化
|
|
398
|
+
watch(() => route.query, () => {
|
|
399
|
+
// 当 query 参数变化时,重新处理
|
|
400
|
+
handleRouteQuery();
|
|
401
|
+
}, { deep: true });
|
|
402
|
+
|
|
403
|
+
// 方法
|
|
404
|
+
async function loadConnections() {
|
|
405
|
+
try {
|
|
406
|
+
const response = await connectionService.getAllConnections();
|
|
407
|
+
connections.value = (response as any)?.data || [];
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error('加载连接失败:', error);
|
|
410
|
+
modal.error(error.msg || error.message || '加载连接失败', {
|
|
411
|
+
operation: 'LOAD_CONNECTIONS',
|
|
412
|
+
stack: error.stack
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function toggleConnection(connection: ConnectionEntity) {
|
|
418
|
+
const connectionId = connection.id || '';
|
|
419
|
+
if (expandedConnections.value.has(connectionId)) {
|
|
420
|
+
expandedConnections.value.delete(connectionId);
|
|
421
|
+
} else {
|
|
422
|
+
expandedConnections.value.add(connectionId);
|
|
423
|
+
loadDatabasesForConnection(connection);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function selectConnection(connection: ConnectionEntity) {
|
|
428
|
+
selectedConnection.value = connection;
|
|
429
|
+
selectedDatabase.value = '';
|
|
430
|
+
selectedTable.value = null;
|
|
431
|
+
activeTab.value = 'overview';
|
|
432
|
+
|
|
433
|
+
// 如果不是从 URL 更新触发的,才更新 URL 参数
|
|
434
|
+
if (!isUpdatingFromUrl) {
|
|
435
|
+
// 更新 URL 参数,只保留 connectionId,移除 database
|
|
436
|
+
// 检查当前 URL 参数是否已经匹配,避免重复更新
|
|
437
|
+
const currentConnectionId = route.query.connectionId as string;
|
|
438
|
+
const currentDatabase = route.query.database as string;
|
|
439
|
+
|
|
440
|
+
if (currentConnectionId !== connection.id || currentDatabase !== undefined) {
|
|
441
|
+
router.push({
|
|
442
|
+
query: {
|
|
443
|
+
connectionId: connection.id
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function loadDatabasesForConnection(connection: ConnectionEntity, forceRefresh = false) {
|
|
451
|
+
const cacheKey = connection.id || '';
|
|
452
|
+
|
|
453
|
+
// 检查缓存
|
|
454
|
+
if (!forceRefresh && databaseCache.value.has(cacheKey)) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!loadingConnections.value.has(cacheKey)) {
|
|
459
|
+
loadingConnections.value.add(cacheKey);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const databases = await databaseService.getDatabases(cacheKey);
|
|
464
|
+
|
|
465
|
+
// 使用新的 Map 实例来触发响应式更新
|
|
466
|
+
const newCache = new Map(databaseCache.value);
|
|
467
|
+
newCache.set(cacheKey, (databases as any)?.data || []);
|
|
468
|
+
databaseCache.value = newCache;
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.error('加载数据库失败:', error);
|
|
471
|
+
|
|
472
|
+
modal.error(error.msg || error.message || '加载数据库失败', {
|
|
473
|
+
operation: 'LOAD_DATABASES',
|
|
474
|
+
connectionId: connection.id,
|
|
475
|
+
stack: error.stack
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const newCache = new Map(databaseCache.value);
|
|
479
|
+
newCache.set(cacheKey, []);
|
|
480
|
+
databaseCache.value = newCache;
|
|
481
|
+
} finally {
|
|
482
|
+
loadingConnections.value.delete(cacheKey);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function toggleDatabase(connection: ConnectionEntity, database: string) {
|
|
487
|
+
const dbKey = `${connection.id}-${database}`;
|
|
488
|
+
if (expandedDatabases.value.has(dbKey)) {
|
|
489
|
+
expandedDatabases.value.delete(dbKey);
|
|
490
|
+
} else {
|
|
491
|
+
expandedDatabases.value.add(dbKey);
|
|
492
|
+
loadDatabaseInfo(connection, database);
|
|
493
|
+
//loadTablesForDatabase(connection, database);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function selectDatabase(connection: ConnectionEntity, database: string) {
|
|
498
|
+
selectedConnection.value = connection;
|
|
499
|
+
selectedDatabase.value = database;
|
|
500
|
+
selectedTable.value = null;
|
|
501
|
+
activeTab.value = 'tables';
|
|
502
|
+
|
|
503
|
+
// 如果不是从 URL 更新触发的,才更新 URL 参数
|
|
504
|
+
if (!isUpdatingFromUrl) {
|
|
505
|
+
// 更新 URL 参数,同时包含 connectionId 和 database
|
|
506
|
+
// 检查当前 URL 参数是否已经匹配,避免重复更新
|
|
507
|
+
const currentConnectionId = route.query.connectionId as string;
|
|
508
|
+
const currentDatabase = route.query.database as string;
|
|
509
|
+
|
|
510
|
+
if (currentConnectionId !== connection.id || currentDatabase !== database) {
|
|
511
|
+
router.push({
|
|
512
|
+
query: {
|
|
513
|
+
connectionId: connection.id,
|
|
514
|
+
database: database
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
loadDatabaseInfo(connection, database);
|
|
521
|
+
loadTablesForDatabase(connection, database, true);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function loadTablesForDatabase(connection: ConnectionEntity, database: string, forceRefresh = false) {
|
|
525
|
+
const dbKey = `${connection.id}-${database}`;
|
|
526
|
+
|
|
527
|
+
if (loadingTables.value.has(dbKey)) return;
|
|
528
|
+
|
|
529
|
+
// 检查缓存
|
|
530
|
+
if (!forceRefresh && tableCache.value.has(dbKey)) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
loadingTables.value.add(dbKey);
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const info = await databaseService.getDatabaseInfo(connection.id, database);
|
|
538
|
+
const tables = info?.data?.tables || [];
|
|
539
|
+
|
|
540
|
+
// 使用新的 Map 实例来触发响应式更新
|
|
541
|
+
const newCache = new Map(tableCache.value);
|
|
542
|
+
newCache.set(dbKey, tables);
|
|
543
|
+
tableCache.value = newCache;
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error('加载表失败:', error);
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
modal.error(error.msg || error.message || '加载表失败', {
|
|
549
|
+
operation: 'LOAD_TABLES',
|
|
550
|
+
database: database,
|
|
551
|
+
stack: error.stack
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const newCache = new Map(tableCache.value);
|
|
555
|
+
newCache.set(dbKey, []);
|
|
556
|
+
tableCache.value = newCache;
|
|
557
|
+
} finally {
|
|
558
|
+
loadingTables.value.delete(dbKey);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function loadDatabaseInfo(connection: ConnectionEntity, database: string, forceRefresh = false) {
|
|
563
|
+
const dbKey = `${connection.id}-${database}`;
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
const info = await databaseService.getDatabaseInfo(connection.id || '', database);
|
|
567
|
+
databaseInfoCache.value.set(dbKey, info.data);
|
|
568
|
+
|
|
569
|
+
loadTablesForDatabase(connection, database, forceRefresh);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
console.error('加载数据库信息失败:', error);
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
modal.error(error.msg || error.message || '加载数据库信息失败', {
|
|
575
|
+
operation: 'LOAD_DATABASE_INFO',
|
|
576
|
+
database: database,
|
|
577
|
+
stack: error.stack
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function selectTable(connection: ConnectionEntity, database: string, table: TableEntity) {
|
|
583
|
+
selectedConnection.value = connection;
|
|
584
|
+
selectedDatabase.value = database;
|
|
585
|
+
selectedTable.value = table;
|
|
586
|
+
activeTableTab.value = 'data';
|
|
587
|
+
|
|
588
|
+
loadTableData(connection, database, table.name);
|
|
589
|
+
loadTableStructure(connection, database, table.name);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function loadTableData(connection: ConnectionEntity, database: string, tableName: string, page: number = 1, pageSize: number = 50, searchQuery?: string) {
|
|
593
|
+
try {
|
|
594
|
+
isGlobalLoading.value = true;
|
|
595
|
+
loadingMessage.value = `正在加载表 "${tableName}" 的数据...`;
|
|
596
|
+
|
|
597
|
+
// 构建WHERE条件用于搜索
|
|
598
|
+
let whereClause = '';
|
|
599
|
+
if (searchQuery && searchQuery.trim()) {
|
|
600
|
+
const searchTerm = searchQuery.trim();
|
|
601
|
+
|
|
602
|
+
// 根据表结构动态构建搜索条件
|
|
603
|
+
if (tableStructure.value?.columns && tableStructure.value.columns.length > 0) {
|
|
604
|
+
const searchableColumns = tableStructure.value.columns.filter((col: any) => {
|
|
605
|
+
// 只搜索字符串类型和数字类型的列
|
|
606
|
+
const type = col.type?.toLowerCase() || '';
|
|
607
|
+
return type.includes('char') || type.includes('text') || type.includes('varchar') ||
|
|
608
|
+
type.includes('int') || type.includes('decimal') || type.includes('float');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
if (searchableColumns.length > 0) {
|
|
612
|
+
const conditions = searchableColumns.map((col: any) => {
|
|
613
|
+
const columnName = col.name;
|
|
614
|
+
const type = col.type?.toLowerCase() || '';
|
|
615
|
+
|
|
616
|
+
if (type.includes('int') || type.includes('decimal') || type.includes('float')) {
|
|
617
|
+
// 数字类型直接比较
|
|
618
|
+
return `${columnName} = '${searchTerm}'`;
|
|
619
|
+
} else {
|
|
620
|
+
// 字符串类型使用LIKE模糊匹配
|
|
621
|
+
return `${columnName} LIKE '%${searchTerm}%'`;
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
whereClause = `WHERE (${conditions.join(' OR ')})`;
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
// 如果没有表结构信息,使用默认搜索条件
|
|
629
|
+
whereClause = `WHERE column1 LIKE '%${searchTerm}%' OR column2 LIKE '%${searchTerm}%'`;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const response = await databaseService.getTableData(
|
|
634
|
+
connection.id,
|
|
635
|
+
database,
|
|
636
|
+
tableName,
|
|
637
|
+
page,
|
|
638
|
+
pageSize,
|
|
639
|
+
whereClause
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// 假设后端返回的数据格式为 { data: [], total: number }
|
|
643
|
+
tableData.value = response?.data?.data || [];
|
|
644
|
+
// 更新总记录数,假设后端返回total字段
|
|
645
|
+
totalRecords.value = parseInt(response?.data?.total) || 0;
|
|
646
|
+
|
|
647
|
+
// 更新当前页码
|
|
648
|
+
currentPage.value = page;
|
|
649
|
+
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error('加载表数据失败:', error);
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
modal.error(error.msg || error.message || '加载表数据失败', {
|
|
655
|
+
operation: 'LOAD_TABLE_DATA',
|
|
656
|
+
table: tableName,
|
|
657
|
+
stack: error.stack
|
|
658
|
+
});
|
|
659
|
+
tableData.value = [];
|
|
660
|
+
} finally {
|
|
661
|
+
isGlobalLoading.value = false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function loadTableStructure(connection: ConnectionEntity, database: string, tableName: string) {
|
|
666
|
+
try {
|
|
667
|
+
const structure = await databaseService.getTableInfo(connection.id, database, tableName);
|
|
668
|
+
tableStructure.value = structure.data;
|
|
669
|
+
tableColumns.value = structure?.data.columns || [];
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.error('加载表结构失败:', error);
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
modal.error(error.msg || error.message || '加载表结构失败', {
|
|
675
|
+
operation: 'LOAD_TABLE_STRUCTURE',
|
|
676
|
+
table: tableName,
|
|
677
|
+
stack: error.stack
|
|
678
|
+
});
|
|
679
|
+
tableStructure.value = null;
|
|
680
|
+
tableColumns.value = [];
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function getDatabasesForConnection(connectionId: string): string[] {
|
|
685
|
+
return databaseCache.value.get(connectionId) || [];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function getTablesForDatabase(connectionId: string | undefined, database: string): TableEntity[] {
|
|
689
|
+
if(!connectionId) return [];
|
|
690
|
+
const dbKey = `${connectionId}-${database}`;
|
|
691
|
+
return tableCache.value.get(dbKey) || [];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function editConnection(connection: ConnectionEntity) {
|
|
695
|
+
connectionEditorRef.value?.showEditModal(connection);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function testConnection(connection: ConnectionEntity) {
|
|
699
|
+
try {
|
|
700
|
+
const result = await connectionService.testConnection(connection);
|
|
701
|
+
|
|
702
|
+
if (result.ret === 0 && result.data) {
|
|
703
|
+
showToast('', `连接 "${connection.name}" 测试成功`, 'success');
|
|
704
|
+
} else {
|
|
705
|
+
showToast('', `连接 "${connection.name}" 测试失败`, 'error');
|
|
706
|
+
}
|
|
707
|
+
} catch (error) {
|
|
708
|
+
|
|
709
|
+
modal.error(error.msg || error.message || '连接测试失败', {
|
|
710
|
+
operation: 'TEST_CONNECTION',
|
|
711
|
+
connectionId: connection.id,
|
|
712
|
+
stack: error.stack
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function showAddConnectionModal() {
|
|
718
|
+
connectionEditorRef.value?.showAddModal();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function onConnectionSaved() {
|
|
722
|
+
loadConnections();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// 连接详情相关事件处理
|
|
726
|
+
function handleTestConnection(connection: ConnectionEntity) {
|
|
727
|
+
testConnection(connection);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function handleEditConnection(connection: ConnectionEntity) {
|
|
731
|
+
editConnection(connection);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function handleRefreshAll(connection: ConnectionEntity) {
|
|
735
|
+
refreshConnection(connection);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function handleCreateDatabase() {
|
|
739
|
+
if (selectedConnection.value) {
|
|
740
|
+
// 刷新数据库缓存
|
|
741
|
+
const connectionId = selectedConnection.value.id || '';
|
|
742
|
+
|
|
743
|
+
// 强制重新加载数据库列表
|
|
744
|
+
if (expandedConnections.value.has(connectionId)) {
|
|
745
|
+
loadDatabasesForConnection(selectedConnection.value, true);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function handleOpenSqlQuery(connection: ConnectionEntity) {
|
|
751
|
+
// TODO: 打开SQL查询界面
|
|
752
|
+
showToast('提示', 'SQL查询功能开发中...', 'info');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function handleExportSchema(connection: ConnectionEntity) {
|
|
756
|
+
// TODO: 导出数据库架构
|
|
757
|
+
showToast('提示', '架构导出功能开发中...', 'info');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function handleViewLogs(connection: ConnectionEntity) {
|
|
761
|
+
// TODO: 查看连接日志
|
|
762
|
+
showToast('提示', '日志查看功能开发中...', 'info');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function refreshAll() {
|
|
766
|
+
databaseCache.value.clear();
|
|
767
|
+
tableCache.value.clear();
|
|
768
|
+
databaseInfoCache.value.clear();
|
|
769
|
+
loadConnections();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function refreshTableData(page: number = 1, pageSize: number = 50, searchQuery?: string) {
|
|
773
|
+
if (selectedConnection.value && selectedDatabase.value && selectedTable.value) {
|
|
774
|
+
await loadTableData(selectedConnection.value, selectedDatabase.value, selectedTable.value.name, page, pageSize, searchQuery);
|
|
775
|
+
// if (!searchQuery) {
|
|
776
|
+
// showToast('', '表数据已刷新', 'success');
|
|
777
|
+
// }
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function handleRefreshStructure() {
|
|
782
|
+
if (selectedConnection.value && selectedDatabase.value && selectedTable.value) {
|
|
783
|
+
await loadTableStructure(selectedConnection.value, selectedDatabase.value, selectedTable.value.name);
|
|
784
|
+
showToast('', '表结构已刷新', 'success');
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function insertNewRow() {
|
|
789
|
+
showToast('提示', '新增功能开发中...', 'info');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function viewTableStructure(connection: ConnectionEntity, database: string, table: TableEntity) {
|
|
793
|
+
selectTable(connection, database, table);
|
|
794
|
+
activeTableTab.value = 'structure';
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function getDbTypeLabel(type: string): string {
|
|
798
|
+
const labelMap: Record<string, string> = {
|
|
799
|
+
mysql: 'MySQL',
|
|
800
|
+
postgres: 'PostgreSQL',
|
|
801
|
+
sqlite: 'SQLite',
|
|
802
|
+
mssql: 'SQL Server',
|
|
803
|
+
oracle: 'Oracle'
|
|
804
|
+
};
|
|
805
|
+
return labelMap[type] || type;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function getDbTypeIconClass(type: string): string {
|
|
809
|
+
const classMap: Record<string, string> = {
|
|
810
|
+
mysql: 'db-mysql',
|
|
811
|
+
postgres: 'db-postgres',
|
|
812
|
+
sqlite: 'db-sqlite',
|
|
813
|
+
mssql: 'db-mssql',
|
|
814
|
+
oracle: 'db-oracle'
|
|
815
|
+
};
|
|
816
|
+
return classMap[type] || 'db-default';
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function getDbLogoClass(type: string): string {
|
|
820
|
+
const classMap: Record<string, string> = {
|
|
821
|
+
mysql: 'db-logo-mysql',
|
|
822
|
+
postgres: 'db-logo-postgres',
|
|
823
|
+
sqlite: 'db-logo-sqlite',
|
|
824
|
+
mssql: 'db-logo-mssql',
|
|
825
|
+
oracle: 'db-logo-oracle'
|
|
826
|
+
};
|
|
827
|
+
return classMap[type] || 'db-logo-default';
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function getDbLogoText(type: string): string {
|
|
831
|
+
const textMap: Record<string, string> = {
|
|
832
|
+
mysql: 'M',
|
|
833
|
+
postgres: 'P',
|
|
834
|
+
sqlite: 'S',
|
|
835
|
+
mssql: 'MS',
|
|
836
|
+
oracle: 'O'
|
|
837
|
+
};
|
|
838
|
+
return textMap[type] || 'D';
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function formatSize(bytes: number): string {
|
|
842
|
+
if (bytes === 0) return '0 B';
|
|
843
|
+
const k = 1024;
|
|
844
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
845
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
846
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function formatNumber(num: number): string {
|
|
850
|
+
return num.toLocaleString();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function formatCellValue(value: any): string {
|
|
854
|
+
if (value === null || value === undefined) return 'NULL';
|
|
855
|
+
if (typeof value === 'string') {
|
|
856
|
+
if (value.length > 50) return value.substring(0, 50) + '...';
|
|
857
|
+
}
|
|
858
|
+
return String(value);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function refreshConnection(connection: ConnectionEntity) {
|
|
862
|
+
// 清除缓存并重新加载
|
|
863
|
+
databaseCache.value.delete(connection.id);
|
|
864
|
+
|
|
865
|
+
// 如果该连接已展开,则重新加载数据库
|
|
866
|
+
if (expandedConnections.value.has(connection.id)) {
|
|
867
|
+
await loadDatabasesForConnection(connection, true);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
showToast('', `连接 "${connection.name}" 已刷新`, 'success');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async function refreshDatabase(connection: ConnectionEntity, database: string) {
|
|
874
|
+
const dbKey = `${connection.id}-${database}`;
|
|
875
|
+
selectedTable.value = null;
|
|
876
|
+
|
|
877
|
+
// 清除缓存
|
|
878
|
+
tableCache.value.delete(dbKey);
|
|
879
|
+
databaseInfoCache.value.delete(dbKey);
|
|
880
|
+
|
|
881
|
+
// 如果数据库已展开,则重新加载表
|
|
882
|
+
if (expandedDatabases.value.has(dbKey)) {
|
|
883
|
+
await loadTablesForDatabase(connection, database, true);
|
|
884
|
+
await loadDatabaseInfo(connection, database);
|
|
885
|
+
} else {
|
|
886
|
+
// 如果数据库未展开,展开它并加载数据
|
|
887
|
+
expandedDatabases.value.add(dbKey);
|
|
888
|
+
await loadDatabaseInfo(connection, database);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
showToast('', `数据库 "${database}" 已刷新`, 'success');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function deleteDatabase(connection: ConnectionEntity, database: string) {
|
|
895
|
+
const result = await modal.confirm(`确定要删除数据库 "${database}" 吗?此操作将删除数据库及其所有数据且不可恢复。`);
|
|
896
|
+
if (result) {
|
|
897
|
+
try {
|
|
898
|
+
isGlobalLoading.value = true;
|
|
899
|
+
loadingMessage.value = `正在删除数据库 "${database}"...`;
|
|
900
|
+
|
|
901
|
+
// 执行删除数据库的SQL
|
|
902
|
+
const deleteSql = `DROP DATABASE \`${database}\``;
|
|
903
|
+
const result = await databaseService.executeQuery(connection.id, deleteSql);
|
|
904
|
+
|
|
905
|
+
if (result.ret === 0) {
|
|
906
|
+
showToast('成功', `数据库 "${database}" 已删除`, 'success');
|
|
907
|
+
|
|
908
|
+
// 清除缓存
|
|
909
|
+
const connectionId = connection.id || '';
|
|
910
|
+
const databases = databaseCache.value.get(connectionId) || [];
|
|
911
|
+
databaseCache.value.set(connectionId, databases.filter(db => db !== database));
|
|
912
|
+
|
|
913
|
+
// 如果删除的是当前选中的数据库,清除选中状态
|
|
914
|
+
if (selectedDatabase.value === database) {
|
|
915
|
+
selectedDatabase.value = '';
|
|
916
|
+
selectedTable.value = null;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 清除相关的缓存
|
|
920
|
+
const dbKey = `${connection.id}-${database}`;
|
|
921
|
+
tableCache.value.delete(dbKey);
|
|
922
|
+
databaseInfoCache.value.delete(dbKey);
|
|
923
|
+
} else {
|
|
924
|
+
showToast('错误', `删除数据库失败: ${result.error}`, 'error');
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
console.error('删除数据库失败:', error);
|
|
928
|
+
|
|
929
|
+
modal.error(error.msg || error.message || '删除数据库失败', {
|
|
930
|
+
operation: 'DELETE_DATABASE',
|
|
931
|
+
database: database,
|
|
932
|
+
stack: error.stack
|
|
933
|
+
});
|
|
934
|
+
} finally {
|
|
935
|
+
isGlobalLoading.value = false;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async function deleteConnection(connection: ConnectionEntity) {
|
|
941
|
+
const result = await modal.confirm(`确定要删除连接 "${connection.name}" 吗?此操作将删除该连接的配置但不会删除实际的数据库数据。`);
|
|
942
|
+
if (result) {
|
|
943
|
+
try {
|
|
944
|
+
isGlobalLoading.value = true;
|
|
945
|
+
loadingMessage.value = `正在删除连接 "${connection.name}"...`;
|
|
946
|
+
|
|
947
|
+
// 删除连接
|
|
948
|
+
await connectionService.deleteConnection(connection.id || '');
|
|
949
|
+
|
|
950
|
+
showToast('成功', `连接 "${connection.name}" 已删除`, 'success');
|
|
951
|
+
|
|
952
|
+
// 从连接列表中移除
|
|
953
|
+
const index = connections.value.findIndex(conn => conn.id === connection.id);
|
|
954
|
+
if (index !== -1) {
|
|
955
|
+
connections.value.splice(index, 1);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// 清除该连接的缓存
|
|
959
|
+
databaseCache.value.delete(connection.id);
|
|
960
|
+
tableCache.value.clear();
|
|
961
|
+
databaseInfoCache.value.clear();
|
|
962
|
+
expandedConnections.value.delete(connection.id);
|
|
963
|
+
|
|
964
|
+
// 如果删除的是当前选中的连接,清除选中状态
|
|
965
|
+
if (selectedConnection.value?.id === connection.id) {
|
|
966
|
+
selectedConnection.value = null;
|
|
967
|
+
selectedDatabase.value = '';
|
|
968
|
+
selectedTable.value = null;
|
|
969
|
+
}
|
|
970
|
+
} catch (error) {
|
|
971
|
+
console.error('删除连接失败:', error);
|
|
972
|
+
|
|
973
|
+
modal.error(error.msg || error.message || '删除连接失败', {
|
|
974
|
+
operation: 'DELETE_CONNECTION',
|
|
975
|
+
connectionId: connection.id,
|
|
976
|
+
stack: error.stack
|
|
977
|
+
});
|
|
978
|
+
} finally {
|
|
979
|
+
isGlobalLoading.value = false;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
async function refreshTable(connection: ConnectionEntity, database: string, table: TableEntity) {
|
|
985
|
+
// 重新加载表数据和结构
|
|
986
|
+
if (selectedTable.value?.name === table.name) {
|
|
987
|
+
await loadTableData(connection, database, table.name);
|
|
988
|
+
await loadTableStructure(connection, database, table.name);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
showToast('', `表 "${table.name}" 已刷新`, 'success');
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// 新增的处理方法
|
|
995
|
+
function handleRefreshDatabase() {
|
|
996
|
+
if (selectedConnection.value && selectedDatabase.value) {
|
|
997
|
+
refreshDatabase(selectedConnection.value, selectedDatabase.value);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async function handleCreateTable(tableData: { name: string; comment: string }) {
|
|
1002
|
+
try {
|
|
1003
|
+
isGlobalLoading.value = true;
|
|
1004
|
+
loadingMessage.value = `正在创建表 "${tableData.name}"...`;
|
|
1005
|
+
|
|
1006
|
+
// TODO: 实现创建表的逻辑
|
|
1007
|
+
console.log('创建表:', tableData);
|
|
1008
|
+
|
|
1009
|
+
// 刷新数据库
|
|
1010
|
+
await handleRefreshDatabase();
|
|
1011
|
+
|
|
1012
|
+
showToast('成功', `表 "${tableData.name}" 创建成功`, 'success');
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
console.error('创建表失败:', error);
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
modal.error(error.msg || error.message || '创建表失败', {
|
|
1018
|
+
operation: 'CREATE_TABLE',
|
|
1019
|
+
stack: error.stack
|
|
1020
|
+
});
|
|
1021
|
+
} finally {
|
|
1022
|
+
isGlobalLoading.value = false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function handleInsertData() {
|
|
1027
|
+
// TODO: 实现插入数据的逻辑
|
|
1028
|
+
showToast('提示', '插入数据功能开发中...', 'info');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function handleExportTable() {
|
|
1032
|
+
// TODO: 实现导出表的逻辑
|
|
1033
|
+
showToast('提示', '导出表功能开发中...', 'info');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async function handleTruncateTable() {
|
|
1037
|
+
if (!selectedTable.value || !selectedConnection.value || !selectedDatabase.value) return;
|
|
1038
|
+
|
|
1039
|
+
const result = await modal.confirm(`确定要清空表 "${selectedTable.value.name}" 吗?此操作将删除所有数据且不可恢复。`);
|
|
1040
|
+
if (result) {
|
|
1041
|
+
try {
|
|
1042
|
+
isGlobalLoading.value = true;
|
|
1043
|
+
loadingMessage.value = `正在清空表 "${selectedTable.value.name}"...`;
|
|
1044
|
+
|
|
1045
|
+
// TODO: 实现清空表的逻辑
|
|
1046
|
+
console.log('清空表:', selectedTable.value.name);
|
|
1047
|
+
|
|
1048
|
+
// 刷新表数据
|
|
1049
|
+
await refreshTableData();
|
|
1050
|
+
|
|
1051
|
+
showToast('成功', `表 "${selectedTable.value.name}" 已清空`, 'success');
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
console.error('清空表失败:', error);
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
modal.error(error.msg || error.message || '清空表失败', {
|
|
1057
|
+
operation: 'TRUNCATE_TABLE',
|
|
1058
|
+
stack: error.stack
|
|
1059
|
+
});
|
|
1060
|
+
} finally {
|
|
1061
|
+
isGlobalLoading.value = false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function handleEditRow(row: any) {
|
|
1067
|
+
// TODO: 实现编辑行的逻辑
|
|
1068
|
+
showToast('提示', '编辑行功能开发中...', 'info');
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function handleDeleteRow(row: any) {
|
|
1072
|
+
// TODO: 实现删除行的逻辑
|
|
1073
|
+
showToast('提示', '删除行功能开发中...', 'info');
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function handleExecuteSql(sql: string) {
|
|
1077
|
+
if (!selectedConnection.value) {
|
|
1078
|
+
showToast('错误', '请先选择数据库连接', 'error');
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (!sql.trim()) {
|
|
1083
|
+
showToast('错误', 'SQL语句不能为空', 'error');
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
try {
|
|
1088
|
+
// 清除之前的结果
|
|
1089
|
+
sqlResult.value = null;
|
|
1090
|
+
sqlExecuting.value = true;
|
|
1091
|
+
|
|
1092
|
+
// 传入当前选中的数据库,如果选中的是表,则使用表所在的数据库
|
|
1093
|
+
const databaseName = selectedTable.value ? selectedDatabase.value : selectedDatabase.value;
|
|
1094
|
+
const result = await databaseService.executeQuery(selectedConnection.value.id, sql, databaseName);
|
|
1095
|
+
|
|
1096
|
+
if (result.ret === 0 && result.data) {
|
|
1097
|
+
showToast('', 'SQL执行成功', 'success');
|
|
1098
|
+
|
|
1099
|
+
// 保存SQL执行结果用于展示
|
|
1100
|
+
if (result.data && Array.isArray(result.data)) {
|
|
1101
|
+
sqlResult.value = {
|
|
1102
|
+
success: true,
|
|
1103
|
+
data: result.data,
|
|
1104
|
+
columns: result.data.length > 0 ? Object.keys(result.data[0]) : [],
|
|
1105
|
+
affectedRows: result.affectedRows || 0,
|
|
1106
|
+
insertId: result.insertId || null
|
|
1107
|
+
};
|
|
1108
|
+
} else {
|
|
1109
|
+
sqlResult.value = {
|
|
1110
|
+
success: true,
|
|
1111
|
+
data: [],
|
|
1112
|
+
columns: [],
|
|
1113
|
+
affectedRows: result.data.affectedRows || 0,
|
|
1114
|
+
insertId: result.data.insertId || null
|
|
1115
|
+
};
|
|
1116
|
+
showToast('', `执行成功,影响行数: ${result.data.affectedRows || 0}`, 'success');
|
|
1117
|
+
}
|
|
1118
|
+
} else {
|
|
1119
|
+
sqlResult.value = {
|
|
1120
|
+
success: false,
|
|
1121
|
+
data: [],
|
|
1122
|
+
columns: [],
|
|
1123
|
+
affectedRows: 0,
|
|
1124
|
+
error: result.error
|
|
1125
|
+
};
|
|
1126
|
+
showToast('错误', `SQL执行失败: ${result.error}`, 'error');
|
|
1127
|
+
}
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
console.error('SQL执行失败:', error);
|
|
1130
|
+
sqlResult.value = {
|
|
1131
|
+
success: false,
|
|
1132
|
+
data: [],
|
|
1133
|
+
columns: [],
|
|
1134
|
+
affectedRows: 0,
|
|
1135
|
+
error: error?.message || ''
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
modal.error(error.msg || error.message || 'SQL执行失败', {
|
|
1140
|
+
operation: 'EXECUTE_SQL',
|
|
1141
|
+
sql: sql,
|
|
1142
|
+
stack: error.stack
|
|
1143
|
+
});
|
|
1144
|
+
} finally {
|
|
1145
|
+
sqlExecuting.value = false;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function showToast(title: string, message: string, type: string = 'success') {
|
|
1150
|
+
toastRef.value?.addToast(title, message, type);
|
|
1151
|
+
}
|
|
1152
|
+
</script>
|
|
1153
|
+
|
|
1154
|
+
<style scoped>
|
|
1155
|
+
.database-explorer {
|
|
1156
|
+
height: 100vh;
|
|
1157
|
+
min-height: 600px;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
.explorer-layout {
|
|
1161
|
+
display: flex;
|
|
1162
|
+
height: 100%;
|
|
1163
|
+
background: white;
|
|
1164
|
+
border-radius: 12px;
|
|
1165
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
1166
|
+
overflow: hidden;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/* 左侧边栏 */
|
|
1170
|
+
.explorer-sidebar {
|
|
1171
|
+
width: 350px;
|
|
1172
|
+
flex-shrink: 0;
|
|
1173
|
+
border-right: 1px solid #e1e5e9;
|
|
1174
|
+
display: flex;
|
|
1175
|
+
flex-direction: column;
|
|
1176
|
+
background: #ffffff;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
.sidebar-header {
|
|
1180
|
+
padding: 1rem 0.875rem;
|
|
1181
|
+
border-bottom: 1px solid #e1e5e9;
|
|
1182
|
+
background: #f8f9fa;
|
|
1183
|
+
display: flex;
|
|
1184
|
+
justify-content: space-between;
|
|
1185
|
+
align-items: center;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
.sidebar-title {
|
|
1189
|
+
margin: 0;
|
|
1190
|
+
font-size: 0.9375rem;
|
|
1191
|
+
font-weight: 600;
|
|
1192
|
+
color: #24292f;
|
|
1193
|
+
display: flex;
|
|
1194
|
+
align-items: center;
|
|
1195
|
+
gap: 0.5rem;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.sidebar-title i {
|
|
1199
|
+
color: #656d76;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
.sidebar-actions {
|
|
1203
|
+
display: flex;
|
|
1204
|
+
gap: 0.25rem;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.sidebar-actions .btn {
|
|
1208
|
+
padding: 0.375rem 0.625rem;
|
|
1209
|
+
font-size: 0.75rem;
|
|
1210
|
+
border: 1px solid #d0d7de;
|
|
1211
|
+
background-color: #ffffff;
|
|
1212
|
+
color: #24292f;
|
|
1213
|
+
border-radius: 6px;
|
|
1214
|
+
transition: all 0.15s ease;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.sidebar-actions .btn:hover {
|
|
1218
|
+
background-color: #f3f4f6;
|
|
1219
|
+
border-color: #d0d7de;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
.sidebar-actions .btn-outline-primary {
|
|
1223
|
+
border-color: #0969da;
|
|
1224
|
+
color: #0969da;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
.sidebar-actions .btn-outline-primary:hover {
|
|
1228
|
+
background-color: #0969da;
|
|
1229
|
+
border-color: #0969da;
|
|
1230
|
+
color: #ffffff;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
.sidebar-actions .btn-outline-success {
|
|
1234
|
+
border-color: #1a7f37;
|
|
1235
|
+
color: #1a7f37;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
.sidebar-actions .btn-outline-success:hover {
|
|
1239
|
+
background-color: #1a7f37;
|
|
1240
|
+
border-color: #1a7f37;
|
|
1241
|
+
color: #ffffff;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.sidebar-content {
|
|
1245
|
+
flex: 1;
|
|
1246
|
+
overflow-y: auto;
|
|
1247
|
+
padding: 0.5rem 0;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/* 树形结构 */
|
|
1251
|
+
.tree-container {
|
|
1252
|
+
font-size: 0.875rem;
|
|
1253
|
+
user-select: none;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
.tree-node {
|
|
1257
|
+
margin-bottom: 0;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
.tree-children {
|
|
1261
|
+
margin-left: 0.75rem;
|
|
1262
|
+
border-left: 1px solid #e1e5e9;
|
|
1263
|
+
padding-left: 0.5rem;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
.node-content {
|
|
1267
|
+
display: flex;
|
|
1268
|
+
align-items: center;
|
|
1269
|
+
padding: 0.375rem 0.75rem;
|
|
1270
|
+
cursor: pointer;
|
|
1271
|
+
transition: background-color 0.15s ease;
|
|
1272
|
+
position: relative;
|
|
1273
|
+
margin-right: 0.5rem;
|
|
1274
|
+
border-radius: 6px;
|
|
1275
|
+
gap: 0.25rem;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.node-expand {
|
|
1279
|
+
width: 16px;
|
|
1280
|
+
height: 16px;
|
|
1281
|
+
display: flex;
|
|
1282
|
+
align-items: center;
|
|
1283
|
+
justify-content: center;
|
|
1284
|
+
flex-shrink: 0;
|
|
1285
|
+
margin-right: 0.25rem;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.node-expand i {
|
|
1289
|
+
font-size: 0.6875rem;
|
|
1290
|
+
color: #656d76;
|
|
1291
|
+
transition: transform 0.15s ease;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
.node-expand i.expanded {
|
|
1295
|
+
transform: rotate(90deg);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.node-main {
|
|
1299
|
+
display: flex;
|
|
1300
|
+
align-items: center;
|
|
1301
|
+
flex: 1;
|
|
1302
|
+
min-width: 0;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.node-content:hover {
|
|
1306
|
+
background-color: #f3f4f6;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
.tree-node.selected .node-content {
|
|
1310
|
+
background-color: #eff1ff;
|
|
1311
|
+
color: #0969da;
|
|
1312
|
+
font-weight: 500;
|
|
1313
|
+
box-shadow: 0 2px 8px rgba(9, 105, 218, 0.15);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
.tree-node.selected .node-content::before {
|
|
1317
|
+
content: '';
|
|
1318
|
+
position: absolute;
|
|
1319
|
+
left: 0;
|
|
1320
|
+
top: 0;
|
|
1321
|
+
bottom: 0;
|
|
1322
|
+
width: 3px;
|
|
1323
|
+
background: linear-gradient(135deg, #0969da, #0550ae);
|
|
1324
|
+
border-radius: 3px 0 0 3px;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.node-icon {
|
|
1328
|
+
width: 20px;
|
|
1329
|
+
display: flex;
|
|
1330
|
+
justify-content: center;
|
|
1331
|
+
flex-shrink: 0;
|
|
1332
|
+
margin-right: 0.5rem;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
.node-icon i {
|
|
1336
|
+
font-size: 0.875rem;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
.node-label {
|
|
1340
|
+
flex: 1;
|
|
1341
|
+
display: flex;
|
|
1342
|
+
align-items: center;
|
|
1343
|
+
gap: 0.375rem;
|
|
1344
|
+
min-width: 0;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
.connection-name,
|
|
1348
|
+
.database-name,
|
|
1349
|
+
.table-name {
|
|
1350
|
+
font-weight: 500;
|
|
1351
|
+
white-space: nowrap;
|
|
1352
|
+
overflow: hidden;
|
|
1353
|
+
text-overflow: ellipsis;
|
|
1354
|
+
line-height: 1.25;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
.tree-node.selected .connection-name,
|
|
1358
|
+
.tree-node.selected .database-name,
|
|
1359
|
+
.tree-node.selected .table-name {
|
|
1360
|
+
font-weight: 600;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
.connection-type,
|
|
1364
|
+
.table-info {
|
|
1365
|
+
font-size: 0.75rem;
|
|
1366
|
+
color: #656d76;
|
|
1367
|
+
background-color: #f6f8fa;
|
|
1368
|
+
padding: 0.125rem 0.375rem;
|
|
1369
|
+
border-radius: 12px;
|
|
1370
|
+
font-weight: 500;
|
|
1371
|
+
white-space: nowrap;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
.tree-node.selected .connection-type,
|
|
1375
|
+
.tree-node.selected .table-info {
|
|
1376
|
+
background-color: rgba(9, 105, 218, 0.1);
|
|
1377
|
+
color: #0969da;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
.node-actions {
|
|
1381
|
+
display: flex;
|
|
1382
|
+
gap: 0.125rem;
|
|
1383
|
+
opacity: 0;
|
|
1384
|
+
transition: opacity 0.15s ease;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
.node-content:hover .node-actions {
|
|
1388
|
+
opacity: 1;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.btn-icon {
|
|
1392
|
+
padding: 0.25rem;
|
|
1393
|
+
background: transparent;
|
|
1394
|
+
border: none;
|
|
1395
|
+
border-radius: 4px;
|
|
1396
|
+
transition: background-color 0.15s ease;
|
|
1397
|
+
display: flex;
|
|
1398
|
+
align-items: center;
|
|
1399
|
+
justify-content: center;
|
|
1400
|
+
width: 20px;
|
|
1401
|
+
height: 20px;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
.btn-icon:hover {
|
|
1405
|
+
background-color: #e1e4e8;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
.btn-icon i {
|
|
1409
|
+
font-size: 0.6875rem;
|
|
1410
|
+
color: #656d76;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
.tree-node.selected .btn-icon:hover {
|
|
1414
|
+
background-color: rgba(9, 105, 218, 0.1);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
.tree-node.selected .btn-icon i {
|
|
1418
|
+
color: #656d76;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.tree-node.selected .btn-icon:hover i {
|
|
1422
|
+
color: #0969da;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.btn-icon-danger:hover {
|
|
1426
|
+
background-color: #fee2e2;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.btn-icon-danger:hover i {
|
|
1430
|
+
color: #dc2626;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
.node-spinner {
|
|
1434
|
+
display: flex;
|
|
1435
|
+
align-items: center;
|
|
1436
|
+
justify-content: center;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.spinner-border-sm {
|
|
1440
|
+
width: 1rem;
|
|
1441
|
+
height: 1rem;
|
|
1442
|
+
border-width: 0.15em;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/* 连接节点样式 */
|
|
1446
|
+
.connection-content {
|
|
1447
|
+
font-weight: 500;
|
|
1448
|
+
color: #24292f;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
.connection-content:hover {
|
|
1452
|
+
background-color: #f3f4f6;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.tree-node.selected .connection-content {
|
|
1456
|
+
background-color: #eff1ff;
|
|
1457
|
+
color: #0969da;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/* 数据库节点样式 */
|
|
1461
|
+
.database-content {
|
|
1462
|
+
color: #24292f;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
.database-content:hover {
|
|
1466
|
+
background-color: #f6f8fa;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
.tree-node.selected .database-content {
|
|
1470
|
+
background-color: #eff1ff;
|
|
1471
|
+
color: #0969da;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/* 表节点样式 */
|
|
1475
|
+
.table-content {
|
|
1476
|
+
color: #24292f;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
.table-content:hover {
|
|
1480
|
+
background-color: #f6f8fa;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
.tree-node.selected .table-content {
|
|
1484
|
+
background-color: #eff1ff;
|
|
1485
|
+
color: #0969da;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/* 数据库品牌色彩 */
|
|
1489
|
+
.db-mysql {
|
|
1490
|
+
color: #00758f !important;
|
|
1491
|
+
background: linear-gradient(135deg, #00758f, #005a70);
|
|
1492
|
+
-webkit-background-clip: text;
|
|
1493
|
+
-webkit-text-fill-color: transparent;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
.db-mysql-bg { background: linear-gradient(135deg, #00758f, #005a70); }
|
|
1497
|
+
|
|
1498
|
+
.db-postgres {
|
|
1499
|
+
color: #336791 !important;
|
|
1500
|
+
background: linear-gradient(135deg, #336791, #2a5278);
|
|
1501
|
+
-webkit-background-clip: text;
|
|
1502
|
+
-webkit-text-fill-color: transparent;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
.db-postgres-bg { background: linear-gradient(135deg, #336791, #2a5278); }
|
|
1506
|
+
|
|
1507
|
+
.db-sqlite {
|
|
1508
|
+
color: #003b57 !important;
|
|
1509
|
+
background: linear-gradient(135deg, #003b57, #002d42);
|
|
1510
|
+
-webkit-background-clip: text;
|
|
1511
|
+
-webkit-text-fill-color: transparent;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
.db-sqlite-bg { background: linear-gradient(135deg, #003b57, #002d42); }
|
|
1515
|
+
|
|
1516
|
+
.db-mssql {
|
|
1517
|
+
color: #cc2927 !important;
|
|
1518
|
+
background: linear-gradient(135deg, #cc2927, #a62220);
|
|
1519
|
+
-webkit-background-clip: text;
|
|
1520
|
+
-webkit-text-fill-color: transparent;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
.db-mssql-bg { background: linear-gradient(135deg, #cc2927, #a62220); }
|
|
1524
|
+
|
|
1525
|
+
.db-oracle {
|
|
1526
|
+
color: #f80000 !important;
|
|
1527
|
+
background: linear-gradient(135deg, #f80000, #d40000);
|
|
1528
|
+
-webkit-background-clip: text;
|
|
1529
|
+
-webkit-text-fill-color: transparent;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
.db-oracle-bg { background: linear-gradient(135deg, #f80000, #d40000); }
|
|
1533
|
+
|
|
1534
|
+
.db-default {
|
|
1535
|
+
color: #64748b !important;
|
|
1536
|
+
background: linear-gradient(135deg, #64748b, #475569);
|
|
1537
|
+
-webkit-background-clip: text;
|
|
1538
|
+
-webkit-text-fill-color: transparent;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
.db-default-bg { background: linear-gradient(135deg, #64748b, #475569); }
|
|
1542
|
+
|
|
1543
|
+
.tree-node.selected .db-mysql,
|
|
1544
|
+
.tree-node.selected .db-postgres,
|
|
1545
|
+
.tree-node.selected .db-sqlite,
|
|
1546
|
+
.tree-node.selected .db-mssql,
|
|
1547
|
+
.tree-node.selected .db-oracle,
|
|
1548
|
+
.tree-node.selected .db-default {
|
|
1549
|
+
color: white !important;
|
|
1550
|
+
background: none;
|
|
1551
|
+
-webkit-text-fill-color: white;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/* 数据库Logo图标 */
|
|
1555
|
+
.db-logo {
|
|
1556
|
+
width: 20px;
|
|
1557
|
+
height: 20px;
|
|
1558
|
+
border-radius: 4px;
|
|
1559
|
+
display: flex;
|
|
1560
|
+
align-items: center;
|
|
1561
|
+
justify-content: center;
|
|
1562
|
+
font-weight: 700;
|
|
1563
|
+
font-size: 0.75rem;
|
|
1564
|
+
color: white;
|
|
1565
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
.db-logo-mysql {
|
|
1569
|
+
background: linear-gradient(135deg, #00758f, #005a70);
|
|
1570
|
+
border: 1px solid #004d61;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
.db-logo-postgres {
|
|
1574
|
+
background: linear-gradient(135deg, #336791, #2a5278);
|
|
1575
|
+
border: 1px solid #244566;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.db-logo-sqlite {
|
|
1579
|
+
background: linear-gradient(135deg, #003b57, #002d42);
|
|
1580
|
+
border: 1px solid #002939;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
.db-logo-mssql {
|
|
1584
|
+
background: linear-gradient(135deg, #cc2927, #a62220);
|
|
1585
|
+
border: 1px solid #8b1f1d;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
.db-logo-oracle {
|
|
1589
|
+
background: linear-gradient(135deg, #f80000, #d40000);
|
|
1590
|
+
border: 1px solid #b30000;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
.db-logo-default {
|
|
1594
|
+
background: linear-gradient(135deg, #64748b, #475569);
|
|
1595
|
+
border: 1px solid #334155;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/* 右侧主内容 */
|
|
1599
|
+
.explorer-main {
|
|
1600
|
+
flex: 1;
|
|
1601
|
+
display: flex;
|
|
1602
|
+
flex-direction: column;
|
|
1603
|
+
overflow: hidden;
|
|
1604
|
+
height: 100%;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/* 详情组件容器 */
|
|
1608
|
+
.explorer-main > * {
|
|
1609
|
+
flex: 1;
|
|
1610
|
+
display: flex;
|
|
1611
|
+
flex-direction: column;
|
|
1612
|
+
height: 100%;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
.content-tabs {
|
|
1616
|
+
flex: 1;
|
|
1617
|
+
display: flex;
|
|
1618
|
+
flex-direction: column;
|
|
1619
|
+
height: 100%;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
.nav-tabs {
|
|
1623
|
+
background: #f8fafc;
|
|
1624
|
+
border-bottom: 1px solid #e2e8f0;
|
|
1625
|
+
padding: 0 1rem;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
.nav-link {
|
|
1629
|
+
border: none;
|
|
1630
|
+
background: transparent;
|
|
1631
|
+
color: #64748b;
|
|
1632
|
+
padding: 1rem 1.5rem;
|
|
1633
|
+
font-weight: 500;
|
|
1634
|
+
transition: all 0.2s ease;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.nav-link:hover {
|
|
1638
|
+
color: #667eea;
|
|
1639
|
+
background: rgba(102, 126, 234, 0.1);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
.nav-link.active {
|
|
1643
|
+
color: #667eea;
|
|
1644
|
+
background: white;
|
|
1645
|
+
border-bottom: 2px solid #667eea;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
.tab-content {
|
|
1649
|
+
flex: 1;
|
|
1650
|
+
overflow-y: auto;
|
|
1651
|
+
padding: 1.5rem;
|
|
1652
|
+
height: calc(100% - 60px); /* 减去导航栏高度 */
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/* 概览样式 */
|
|
1656
|
+
.overview-section {
|
|
1657
|
+
padding: 1rem 0;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
.info-cards {
|
|
1661
|
+
display: grid;
|
|
1662
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
1663
|
+
gap: 1rem;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
.info-card {
|
|
1667
|
+
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
|
1668
|
+
border-radius: 12px;
|
|
1669
|
+
padding: 1.5rem;
|
|
1670
|
+
display: flex;
|
|
1671
|
+
align-items: center;
|
|
1672
|
+
gap: 1rem;
|
|
1673
|
+
border: 1px solid #e2e8f0;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
.card-icon {
|
|
1677
|
+
width: 48px;
|
|
1678
|
+
height: 48px;
|
|
1679
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1680
|
+
border-radius: 12px;
|
|
1681
|
+
display: flex;
|
|
1682
|
+
align-items: center;
|
|
1683
|
+
justify-content: center;
|
|
1684
|
+
color: white;
|
|
1685
|
+
font-size: 1.25rem;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.card-content h6 {
|
|
1689
|
+
margin: 0 0 0.25rem 0;
|
|
1690
|
+
color: #64748b;
|
|
1691
|
+
font-size: 0.875rem;
|
|
1692
|
+
font-weight: 500;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
.card-value {
|
|
1696
|
+
margin: 0;
|
|
1697
|
+
color: #1e293b;
|
|
1698
|
+
font-weight: 600;
|
|
1699
|
+
font-size: 1.1rem;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/* 表格网格 */
|
|
1703
|
+
.table-grid {
|
|
1704
|
+
display: grid;
|
|
1705
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
1706
|
+
gap: 1rem;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
.table-card {
|
|
1710
|
+
background: white;
|
|
1711
|
+
border: 1px solid #e2e8f0;
|
|
1712
|
+
border-radius: 12px;
|
|
1713
|
+
padding: 1rem;
|
|
1714
|
+
cursor: pointer;
|
|
1715
|
+
transition: all 0.2s ease;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
.table-card:hover {
|
|
1719
|
+
border-color: #667eea;
|
|
1720
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
|
1721
|
+
transform: translateY(-2px);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
.table-card .card-header {
|
|
1725
|
+
display: flex;
|
|
1726
|
+
align-items: center;
|
|
1727
|
+
gap: 0.75rem;
|
|
1728
|
+
margin-bottom: 0.75rem;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
.table-icon {
|
|
1732
|
+
width: 32px;
|
|
1733
|
+
height: 32px;
|
|
1734
|
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
1735
|
+
border-radius: 8px;
|
|
1736
|
+
display: flex;
|
|
1737
|
+
align-items: center;
|
|
1738
|
+
justify-content: center;
|
|
1739
|
+
color: white;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
.table-name {
|
|
1743
|
+
font-weight: 600;
|
|
1744
|
+
color: #1e293b;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
.table-stats {
|
|
1748
|
+
display: flex;
|
|
1749
|
+
gap: 1rem;
|
|
1750
|
+
margin-bottom: 0.5rem;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
.stat {
|
|
1754
|
+
display: flex;
|
|
1755
|
+
flex-direction: column;
|
|
1756
|
+
gap: 0.25rem;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
.stat-label {
|
|
1760
|
+
font-size: 0.75rem;
|
|
1761
|
+
color: #64748b;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
.stat-value {
|
|
1765
|
+
font-size: 0.875rem;
|
|
1766
|
+
font-weight: 600;
|
|
1767
|
+
color: #1e293b;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
.table-comment {
|
|
1771
|
+
font-size: 0.75rem;
|
|
1772
|
+
color: #64748b;
|
|
1773
|
+
font-style: italic;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/* 数据工具栏 */
|
|
1777
|
+
.data-toolbar {
|
|
1778
|
+
display: flex;
|
|
1779
|
+
justify-content: space-between;
|
|
1780
|
+
align-items: center;
|
|
1781
|
+
margin-bottom: 1rem;
|
|
1782
|
+
padding: 0.75rem;
|
|
1783
|
+
background: #f8fafc;
|
|
1784
|
+
border-radius: 8px;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
.toolbar-left,
|
|
1788
|
+
.toolbar-right {
|
|
1789
|
+
display: flex;
|
|
1790
|
+
gap: 0.5rem;
|
|
1791
|
+
align-items: center;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/* 空状态 */
|
|
1795
|
+
.empty-state,
|
|
1796
|
+
.default-state {
|
|
1797
|
+
display: flex;
|
|
1798
|
+
flex-direction: column;
|
|
1799
|
+
align-items: center;
|
|
1800
|
+
justify-content: center;
|
|
1801
|
+
height: 100%;
|
|
1802
|
+
color: #64748b;
|
|
1803
|
+
text-align: center;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
.empty-icon,
|
|
1807
|
+
.default-icon {
|
|
1808
|
+
font-size: 3rem;
|
|
1809
|
+
margin-bottom: 1rem;
|
|
1810
|
+
opacity: 0.5;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
.empty-text h5,
|
|
1814
|
+
.default-content h5 {
|
|
1815
|
+
color: #64748b;
|
|
1816
|
+
margin-bottom: 0.5rem;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/* 响应式 */
|
|
1820
|
+
@media (max-width: 768px) {
|
|
1821
|
+
.explorer-layout {
|
|
1822
|
+
flex-direction: column;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
.explorer-sidebar {
|
|
1826
|
+
width: 100%;
|
|
1827
|
+
height: 40vh;
|
|
1828
|
+
border-right: none;
|
|
1829
|
+
border-bottom: 1px solid #e2e8f0;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
.info-cards {
|
|
1833
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
.table-grid {
|
|
1837
|
+
grid-template-columns: 1fr;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
</style>
|