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.
Files changed (125) hide show
  1. package/.dockerignore +21 -0
  2. package/.editorconfig +11 -0
  3. package/.eslintrc.cjs +14 -0
  4. package/.eslintrc.json +7 -0
  5. package/.prettierrc.js +3 -0
  6. package/.tpl.env +22 -0
  7. package/README.md +260 -0
  8. package/bin/build.sh +28 -0
  9. package/bin/deploy.sh +8 -0
  10. package/bin/dev.sh +10 -0
  11. package/bin/docker/.env +4 -0
  12. package/bin/docker/dev-docker-compose.yml +43 -0
  13. package/bin/docker/dev.Dockerfile +24 -0
  14. package/bin/docker/prod-docker-compose.yml +17 -0
  15. package/bin/docker/prod.Dockerfile +29 -0
  16. package/bin/fdb2.js +142 -0
  17. package/data/connections.demo.json +32 -0
  18. package/env.d.ts +1 -0
  19. package/nw-build.js +120 -0
  20. package/nw-dev.js +65 -0
  21. package/package.json +114 -0
  22. package/public/favicon.ico +0 -0
  23. package/public/index.html +9 -0
  24. package/public/modules/header.tpl +14 -0
  25. package/public/modules/initial_state.tpl +55 -0
  26. package/server/index.ts +677 -0
  27. package/server/model/connection.entity.ts +66 -0
  28. package/server/model/database.entity.ts +246 -0
  29. package/server/service/connection.service.ts +334 -0
  30. package/server/service/database/base.service.ts +363 -0
  31. package/server/service/database/database.service.ts +510 -0
  32. package/server/service/database/index.ts +7 -0
  33. package/server/service/database/mssql.service.ts +723 -0
  34. package/server/service/database/mysql.service.ts +761 -0
  35. package/server/service/database/oracle.service.ts +839 -0
  36. package/server/service/database/postgres.service.ts +744 -0
  37. package/server/service/database/sqlite.service.ts +559 -0
  38. package/server/service/session.service.ts +158 -0
  39. package/server.js +128 -0
  40. package/src/adapter/ajax.ts +135 -0
  41. package/src/assets/base.css +1 -0
  42. package/src/assets/database.css +950 -0
  43. package/src/assets/images/collapse.png +0 -0
  44. package/src/assets/images/no-login.png +0 -0
  45. package/src/assets/images/svg/illustrations/illustration-1.svg +1 -0
  46. package/src/assets/images/svg/illustrations/illustration-2.svg +2 -0
  47. package/src/assets/images/svg/illustrations/illustration-3.svg +50 -0
  48. package/src/assets/images/svg/illustrations/illustration-4.svg +1 -0
  49. package/src/assets/images/svg/illustrations/illustration-5.svg +73 -0
  50. package/src/assets/images/svg/illustrations/illustration-6.svg +89 -0
  51. package/src/assets/images/svg/illustrations/illustration-7.svg +39 -0
  52. package/src/assets/images/svg/illustrations/illustration-8.svg +1 -0
  53. package/src/assets/images/svg/separators/curve-2.svg +3 -0
  54. package/src/assets/images/svg/separators/curve.svg +3 -0
  55. package/src/assets/images/svg/separators/line.svg +3 -0
  56. package/src/assets/images/theme/light/screen-1-1000x800.jpg +0 -0
  57. package/src/assets/images/theme/light/screen-2-1000x800.jpg +0 -0
  58. package/src/assets/login/bg.jpg +0 -0
  59. package/src/assets/login/bg.png +0 -0
  60. package/src/assets/login/left.jpg +0 -0
  61. package/src/assets/logo.svg +73 -0
  62. package/src/assets/logo.webp +0 -0
  63. package/src/assets/main.css +1 -0
  64. package/src/base/config.ts +20 -0
  65. package/src/base/detect.ts +134 -0
  66. package/src/base/entity.ts +92 -0
  67. package/src/base/eventBus.ts +37 -0
  68. package/src/base//345/237/272/347/241/200/345/261/202.md +7 -0
  69. package/src/components/connection-editor/index.vue +590 -0
  70. package/src/components/dataGrid/index.vue +105 -0
  71. package/src/components/dataGrid/pagination.vue +106 -0
  72. package/src/components/loading/index.vue +43 -0
  73. package/src/components/modal/index.ts +181 -0
  74. package/src/components/modal/index.vue +560 -0
  75. package/src/components/toast/index.ts +44 -0
  76. package/src/components/toast/toast.vue +58 -0
  77. package/src/components/user/name.vue +104 -0
  78. package/src/components/user/selector.vue +416 -0
  79. package/src/domain/SysConfig.ts +74 -0
  80. package/src/platform/App.vue +8 -0
  81. package/src/platform/database/components/connection-detail.vue +1154 -0
  82. package/src/platform/database/components/data-editor.vue +478 -0
  83. package/src/platform/database/components/data-import-export.vue +1602 -0
  84. package/src/platform/database/components/database-detail.vue +1173 -0
  85. package/src/platform/database/components/database-monitor.vue +1086 -0
  86. package/src/platform/database/components/db-tools.vue +577 -0
  87. package/src/platform/database/components/query-history.vue +1349 -0
  88. package/src/platform/database/components/sql-executor.vue +738 -0
  89. package/src/platform/database/components/sql-query-editor.vue +1046 -0
  90. package/src/platform/database/components/table-detail.vue +1376 -0
  91. package/src/platform/database/components/table-editor.vue +690 -0
  92. package/src/platform/database/explorer.vue +1840 -0
  93. package/src/platform/database/index.vue +1193 -0
  94. package/src/platform/database/layout.vue +367 -0
  95. package/src/platform/database/router.ts +37 -0
  96. package/src/platform/database/styles/common.scss +602 -0
  97. package/src/platform/database/types/common.ts +445 -0
  98. package/src/platform/database/utils/export.ts +232 -0
  99. package/src/platform/database/utils/helpers.ts +437 -0
  100. package/src/platform/index.ts +33 -0
  101. package/src/platform/router.ts +41 -0
  102. package/src/service/base.ts +128 -0
  103. package/src/service/database.ts +500 -0
  104. package/src/service/login.ts +121 -0
  105. package/src/shims-vue.d.ts +7 -0
  106. package/src/stores/connection.ts +266 -0
  107. package/src/stores/session.ts +87 -0
  108. package/src/typings/database-types.ts +413 -0
  109. package/src/typings/database.ts +364 -0
  110. package/src/typings/global.d.ts +58 -0
  111. package/src/typings/pinia.d.ts +8 -0
  112. package/src/utils/clipboard.ts +30 -0
  113. package/src/utils/database-types.ts +243 -0
  114. package/src/utils/modal.ts +124 -0
  115. package/src/utils/request.ts +55 -0
  116. package/src/utils/sleep.ts +4 -0
  117. package/src/utils/toast.ts +73 -0
  118. package/src/utils/util.ts +171 -0
  119. package/src/utils/xlsx.ts +228 -0
  120. package/tsconfig.json +33 -0
  121. package/tsconfig.server.json +19 -0
  122. package/view/index.html +9 -0
  123. package/view/modules/header.tpl +14 -0
  124. package/view/modules/initial_state.tpl +20 -0
  125. package/vite.config.ts +384 -0
@@ -0,0 +1,1154 @@
1
+ <template>
2
+ <div class="connection-detail">
3
+ <!-- 连接头部信息 -->
4
+ <div class="connection-header">
5
+ <div class="connection-info">
6
+ <div class="connection-avatar">
7
+ <div class="db-logo" :class="getDbLogoClass(connection?.type)">
8
+ {{ getDbLogoText(connection?.type) }}
9
+ </div>
10
+ </div>
11
+ <div class="connection-meta">
12
+ <h4 class="connection-name">{{ connection?.name }}</h4>
13
+ <div class="connection-type-info">
14
+ <span class="db-type">{{ getDbTypeLabel(connection?.type) }}</span>
15
+ <span class="connection-status" :class="connectionStatusClass">
16
+ <div class="status-dot"></div>
17
+ {{ connectionStatusText }}
18
+ </span>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ <div class="connection-actions">
23
+ <button class="btn btn-sm btn-outline-info" @click="toggleDetails" title="查看/隐藏详细信息">
24
+ <i class="bi" :class="isDetailsExpanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i> 详情
25
+ </button>
26
+ <button class="btn btn-sm btn-outline-primary" @click="testConnection">
27
+ <i class="bi bi-wifi"></i>
28
+ </button>
29
+ <button class="btn btn-sm btn-outline-secondary" @click="editConnection">
30
+ <i class="bi bi-pencil"></i> 编辑
31
+ </button>
32
+ </div>
33
+ </div>
34
+
35
+ <!-- 连接详情卡片 -->
36
+ <div v-if="isDetailsExpanded" class="connection-details-panel">
37
+ <div class="detail-card">
38
+ <div class="card-body">
39
+ <div class="row">
40
+ <!-- 基本信息 -->
41
+ <div class="col-md-6">
42
+ <h6 class="section-title">
43
+ <i class="bi bi-info-circle"></i>
44
+ 基本信息
45
+ </h6>
46
+ <div class="info-grid">
47
+ <div class="info-item">
48
+ <label class="info-label">主机地址</label>
49
+ <div class="info-value">{{ connection?.host }}</div>
50
+ </div>
51
+ <div class="info-item">
52
+ <label class="info-label">端口</label>
53
+ <div class="info-value">{{ connection?.port }}</div>
54
+ </div>
55
+ <div class="info-item">
56
+ <label class="info-label">用户名</label>
57
+ <div class="info-value">{{ connection?.username }}</div>
58
+ </div>
59
+ <div class="info-item">
60
+ <label class="info-label">数据库类型</label>
61
+ <div class="info-value">
62
+ <span class="db-type-badge" :class="getDbLogoClass(connection?.type)">
63
+ {{ getDbTypeLabel(connection?.type) }}
64
+ </span>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- 连接统计 -->
71
+ <div class="col-md-6">
72
+ <h6 class="section-title">
73
+ <i class="bi bi-bar-chart"></i>
74
+ 连接统计
75
+ </h6>
76
+ <div class="stats-grid">
77
+ <div class="stat-item">
78
+ <div class="stat-value">{{ connectionStats.databaseCount || 0 }}</div>
79
+ <div class="stat-label">数据库数量</div>
80
+ </div>
81
+ <div class="stat-item">
82
+ <div class="stat-value">{{ connectionStats.tableCount || 0 }}</div>
83
+ <div class="stat-label">表总数</div>
84
+ </div>
85
+ <div class="stat-item">
86
+ <div class="stat-value">{{ formatFileSize(connectionStats.totalSize || 0) }}</div>
87
+ <div class="stat-label">总大小</div>
88
+ </div>
89
+ <div class="stat-item">
90
+ <div class="stat-value">{{ connectionStats.lastConnected || '从未' }}</div>
91
+ <div class="stat-label">最后连接</div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- 快速操作 -->
101
+ <div class="quick-actions">
102
+ <div class="actions-header">
103
+ <h6 class="actions-title">
104
+ <i class="bi bi-lightning"></i>
105
+ 快速操作
106
+ </h6>
107
+ </div>
108
+ <div class="actions-grid">
109
+ <button class="action-btn" @click="showCreateDatabaseModal">
110
+ <div class="action-icon">
111
+ <i class="bi bi-plus-circle"></i>
112
+ </div>
113
+ <div class="action-text">创建数据库</div>
114
+ </button>
115
+ <!-- <button class="action-btn" @click="refreshAll">
116
+ <div class="action-icon">
117
+ <i class="bi bi-arrow-clockwise"></i>
118
+ </div>
119
+ <div class="action-text">刷新所有数据库</div>
120
+ </button> -->
121
+ <!-- <button class="action-btn" @click="activeTab = 'sql'">
122
+ <div class="action-icon">
123
+ <i class="bi bi-terminal"></i>
124
+ </div>
125
+ <div class="action-text">SQL查询</div>
126
+ </button> -->
127
+ <button class="action-btn" @click="exportSchema">
128
+ <div class="action-icon">
129
+ <i class="bi bi-download"></i>
130
+ </div>
131
+ <div class="action-text">导出架构</div>
132
+ </button>
133
+ <button class="action-btn" @click="viewLogs">
134
+ <div class="action-icon">
135
+ <i class="bi bi-file-text"></i>
136
+ </div>
137
+ <div class="action-text">查看日志</div>
138
+ </button>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- 标签页 -->
143
+ <div class="connection-tabs">
144
+ <ul class="nav nav-tabs">
145
+ <li class="nav-item">
146
+ <button
147
+ class="nav-link"
148
+ :class="{ active: activeTab === 'databases' }"
149
+ @click="activeTab = 'databases'"
150
+ >
151
+ <i class="bi bi-database-fill"></i> 数据库列表
152
+ <span class="badge bg-primary ms-1">{{ databases.length }}</span>
153
+ </button>
154
+ </li>
155
+ <li class="nav-item">
156
+ <button
157
+ class="nav-link"
158
+ :class="{ active: activeTab === 'sql' }"
159
+ @click="activeTab = 'sql'"
160
+ >
161
+ <i class="bi bi-terminal"></i> SQL查询
162
+ </button>
163
+ </li>
164
+ </ul>
165
+
166
+ <div class="tab-content">
167
+ <!-- 数据库列表标签页 -->
168
+ <div v-show="activeTab === 'databases'" class="tab-panel">
169
+ <div class="databases-section">
170
+ <div class="section-header">
171
+ <div class="section-actions">
172
+ <div class="search-box">
173
+ <input
174
+ type="text"
175
+ class="form-control form-control-sm"
176
+ v-model="searchKeyword"
177
+ placeholder="搜索数据库..."
178
+ style="width: 200px;"
179
+ >
180
+ </div>
181
+ <button class="btn btn-sm btn-outline-primary ms-2" @click="loadDatabases" :disabled="loadingDatabases">
182
+ <span v-if="loadingDatabases" class="spinner-border spinner-border-sm me-1"></span>
183
+ <i class="bi bi-arrow-clockwise"></i> 刷新
184
+ </button>
185
+ </div>
186
+ </div>
187
+
188
+ <div class="databases-list">
189
+ <div v-if="loadingDatabases" class="loading-state">
190
+ <div class="spinner-border text-primary"></div>
191
+ <span>加载数据库列表...</span>
192
+ </div>
193
+ <div v-else-if="filteredDatabases.length === 0" class="empty-state">
194
+ <i class="bi bi-database"></i>
195
+ <p>{{ searchKeyword ? '没有找到匹配的数据库' : '暂无数据库' }}</p>
196
+ <button class="btn btn-sm btn-primary" @click="showCreateDatabaseModal">
197
+ <i class="bi bi-plus"></i> 创建数据库
198
+ </button>
199
+ </div>
200
+ <div v-else class="databases-list-simple">
201
+ <div
202
+ v-for="database in filteredDatabases"
203
+ :key="database.name"
204
+ class="database-item"
205
+ @click="openDatabase(database)"
206
+ >
207
+ <div class="database-item-icon">
208
+ <i class="bi bi-database"></i>
209
+ </div>
210
+ <div class="database-item-name">{{ database.name }}</div>
211
+ <div class="database-item-actions">
212
+ <button class="btn btn-sm btn-outline-secondary" @click.stop="activeTab = 'sql'">
213
+ <i class="bi bi-terminal"></i>
214
+ </button>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ <!-- SQL查询标签页 -->
223
+ <div v-show="activeTab === 'sql'" class="tab-panel">
224
+ <div class="sql-section">
225
+ <div class="sql-header">
226
+ <h6 class="sql-title">
227
+ <i class="bi bi-terminal"></i>
228
+ SQL查询
229
+ </h6>
230
+ <div class="sql-db-info" v-if="props.connection">
231
+ <span class="badge bg-info">{{ props.connection.name }}</span>
232
+ </div>
233
+ </div>
234
+ <SqlExecutor
235
+ :connection="props.connection"
236
+ :database="''"
237
+ />
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <!-- 创建数据库模态框 -->
244
+ <div v-if="showCreateDatabase" class="modal fade show d-block" style="background: rgba(0,0,0,0.5);">
245
+ <div class="modal-dialog">
246
+ <div class="modal-content">
247
+ <div class="modal-header">
248
+ <h5 class="modal-title">创建数据库</h5>
249
+ <button type="button" class="btn-close" @click="showCreateDatabase = false"></button>
250
+ </div>
251
+ <div class="modal-body">
252
+ <form>
253
+ <div class="mb-3">
254
+ <label class="form-label">数据库名称 <span class="text-danger">*</span></label>
255
+ <input type="text" class="form-control" v-model="newDatabase.name" placeholder="输入数据库名称" required>
256
+ </div>
257
+
258
+ <!-- MySQL特定选项 -->
259
+ <div v-if="connection?.type === 'mysql'">
260
+ <div class="mb-3">
261
+ <label class="form-label">字符集</label>
262
+ <select class="form-select" v-model="newDatabase.options.charset">
263
+ <option value="">默认</option>
264
+ <option value="utf8mb4">utf8mb4</option>
265
+ <option value="utf8">utf8</option>
266
+ <option value="latin1">latin1</option>
267
+ </select>
268
+ </div>
269
+ <div class="mb-3">
270
+ <label class="form-label">排序规则</label>
271
+ <select class="form-select" v-model="newDatabase.options.collation">
272
+ <option value="">默认</option>
273
+ <option value="utf8mb4_unicode_ci">utf8mb4_unicode_ci</option>
274
+ <option value="utf8mb4_general_ci">utf8mb4_general_ci</option>
275
+ <option value="utf8_unicode_ci">utf8_unicode_ci</option>
276
+ </select>
277
+ </div>
278
+ </div>
279
+
280
+ <!-- PostgreSQL特定选项 -->
281
+ <div v-if="connection?.type === 'postgres'">
282
+ <div class="mb-3">
283
+ <label class="form-label">所有者</label>
284
+ <input type="text" class="form-control" v-model="newDatabase.options.owner" placeholder="数据库所有者">
285
+ </div>
286
+ <div class="mb-3">
287
+ <label class="form-label">模板</label>
288
+ <input type="text" class="form-control" v-model="newDatabase.options.template" placeholder="模板数据库">
289
+ </div>
290
+ <div class="mb-3">
291
+ <label class="form-label">编码</label>
292
+ <select class="form-select" v-model="newDatabase.options.encoding">
293
+ <option value="">默认</option>
294
+ <option value="UTF8">UTF8</option>
295
+ <option value="LATIN1">LATIN1</option>
296
+ </select>
297
+ </div>
298
+ <div class="mb-3">
299
+ <label class="form-label">表空间</label>
300
+ <input type="text" class="form-control" v-model="newDatabase.options.tablespace" placeholder="表空间">
301
+ </div>
302
+ </div>
303
+
304
+ <!-- SQL Server特定选项 -->
305
+ <div v-if="connection?.type === 'mssql'">
306
+ <div class="mb-3">
307
+ <label class="form-label">排序规则</label>
308
+ <input type="text" class="form-control" v-model="newDatabase.options.collation" placeholder="排序规则">
309
+ </div>
310
+ </div>
311
+ </form>
312
+ </div>
313
+ <div class="modal-footer">
314
+ <button type="button" class="btn btn-secondary" @click="showCreateDatabase = false">取消</button>
315
+ <button type="button" class="btn btn-primary" @click="createDatabase" :disabled="!newDatabase.name || creatingDatabase">
316
+ <span v-if="creatingDatabase" class="spinner-border spinner-border-sm me-2"></span>
317
+ 创建
318
+ </button>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ </template>
325
+
326
+ <script lang="ts" setup>
327
+ import { ref, computed, onMounted, watch } from 'vue';
328
+ import { useRouter } from 'vue-router';
329
+ import type { ConnectionEntity } from '@/typings/database';
330
+ import { useConnectionStore } from '@/stores/connection';
331
+ import SqlExecutor from './sql-executor.vue';
332
+ import { modal } from '@/utils/modal';
333
+
334
+ const router = useRouter();
335
+
336
+ const props = defineProps<{
337
+ connection: ConnectionEntity | null;
338
+ }>();
339
+
340
+ const emit = defineEmits<{
341
+ 'test-connection': [connection: ConnectionEntity];
342
+ 'edit-connection': [connection: ConnectionEntity];
343
+ 'refresh-all': [connection: ConnectionEntity];
344
+ 'open-sql-query': [connection: ConnectionEntity];
345
+ 'export-schema': [connection: ConnectionEntity];
346
+ 'view-logs': [connection: ConnectionEntity];
347
+ 'create-database': [];
348
+ 'open-database': [connection: ConnectionEntity, database: string];
349
+ }>();
350
+
351
+ // 初始化连接 store
352
+ const connectionStore = useConnectionStore();
353
+
354
+ // 连接统计信息
355
+ const connectionStats = ref({
356
+ databaseCount: 0,
357
+ tableCount: 0,
358
+ totalSize: 0,
359
+ lastConnected: null as string | null
360
+ });
361
+
362
+ // 连接状态: 'unknown' | 'connected' | 'disconnected' | 'testing'
363
+ const connectionStatus = ref<'unknown' | 'connected' | 'disconnected' | 'testing'>('unknown');
364
+
365
+ // 搜索关键字
366
+ const searchKeyword = ref('');
367
+
368
+ // 折叠状态
369
+ const isDetailsExpanded = ref(false);
370
+
371
+ // 切换折叠状态
372
+ function toggleDetails() {
373
+ isDetailsExpanded.value = !isDetailsExpanded.value;
374
+ }
375
+
376
+ // 创建数据库相关
377
+ const showCreateDatabase = ref(false);
378
+ const creatingDatabase = ref(false);
379
+ const newDatabase = ref({
380
+ name: '',
381
+ options: {
382
+ charset: '',
383
+ collation: '',
384
+ owner: '',
385
+ template: '',
386
+ encoding: '',
387
+ tablespace: ''
388
+ }
389
+ });
390
+
391
+ // 标签页状态
392
+ const activeTab = ref('databases');
393
+
394
+ // 监听连接变化
395
+ watch(() => props.connection, (newConnection) => {
396
+ connectionStore.setCurrentConnection(newConnection);
397
+ if (newConnection) {
398
+ loadConnectionStats();
399
+ // 重置连接状态为未知
400
+ connectionStatus.value = 'unknown';
401
+ // 自动测试连接
402
+ testConnection();
403
+ }
404
+ }, { immediate: true });
405
+
406
+ // 生命周期
407
+ onMounted(() => {
408
+ if (props.connection) {
409
+ loadConnectionStats();
410
+ }
411
+ });
412
+
413
+ // 数据库列表(从 store 获取)
414
+ const databases = computed(() => connectionStore.databases);
415
+ const loadingDatabases = computed(() => connectionStore.isLoadingDatabases);
416
+
417
+ // 连接状态的显示类和文本
418
+ const connectionStatusClass = computed(() => {
419
+ switch (connectionStatus.value) {
420
+ case 'connected':
421
+ return 'status-online';
422
+ case 'disconnected':
423
+ return 'status-offline';
424
+ case 'testing':
425
+ return 'status-testing';
426
+ default:
427
+ return 'status-offline';
428
+ }
429
+ });
430
+
431
+ const connectionStatusText = computed(() => {
432
+ switch (connectionStatus.value) {
433
+ case 'connected':
434
+ return '已连接';
435
+ case 'disconnected':
436
+ return '未连接';
437
+ case 'testing':
438
+ return '测试中...';
439
+ default:
440
+ return '未知';
441
+ }
442
+ });
443
+
444
+ // 方法
445
+ function loadConnectionStats() {
446
+ // 模拟加载连接统计信息
447
+ connectionStats.value = {
448
+ databaseCount: connectionStore.databaseCount,
449
+ tableCount: connectionStore.tableCount,
450
+ totalSize: 1024 * 1024 * 512, // 512MB
451
+ lastConnected: '2小时前'
452
+ };
453
+ }
454
+
455
+ function loadDatabases() {
456
+ connectionStore.loadDatabases();
457
+ }
458
+
459
+ const filteredDatabases = computed(() => {
460
+ if (!searchKeyword.value) {
461
+ return databases.value;
462
+ }
463
+ const keyword = searchKeyword.value.toLowerCase();
464
+ return databases.value.filter(db =>
465
+ db.name.toLowerCase().includes(keyword)
466
+ );
467
+ });
468
+
469
+ function openDatabase(database: any) {
470
+ if (props.connection) {
471
+ emit('open-database', props.connection, database.name);
472
+ router.push({
473
+ path: `/database/explorer`,
474
+ query: { connectionId: props.connection.id, database: database.name }
475
+ });
476
+ }
477
+ }
478
+
479
+ async function testConnection() {
480
+ if (!props.connection) return;
481
+
482
+ connectionStatus.value = 'testing';
483
+
484
+ try {
485
+ const isConnected = await connectionStore.testConnection(props.connection);
486
+ connectionStatus.value = isConnected ? 'connected' : 'disconnected';
487
+ } catch (error) {
488
+ console.error('连接测试失败:', error);
489
+ connectionStatus.value = 'disconnected';
490
+ }
491
+ }
492
+
493
+ function editConnection() {
494
+ if (props.connection) {
495
+ emit('edit-connection', props.connection);
496
+ }
497
+ }
498
+
499
+ function refreshAll() {
500
+ if (props.connection) {
501
+ emit('refresh-all', props.connection);
502
+ }
503
+ }
504
+
505
+ function exportSchema() {
506
+ if (props.connection) {
507
+ emit('export-schema', props.connection);
508
+ }
509
+ }
510
+
511
+ function viewLogs() {
512
+ if (props.connection) {
513
+ emit('view-logs', props.connection);
514
+ }
515
+ }
516
+
517
+ function showCreateDatabaseModal() {
518
+ emit('create-database');
519
+ showCreateDatabase.value = true;
520
+ }
521
+
522
+ async function createDatabase() {
523
+ if (!newDatabase.value.name) {
524
+ modal.warning('请输入数据库名称');
525
+ return;
526
+ }
527
+
528
+ if (props.connection) {
529
+ creatingDatabase.value = true;
530
+ try {
531
+ await connectionStore.createDatabase(newDatabase.value.name);
532
+ modal.success('数据库创建成功');
533
+ showCreateDatabase.value = false;
534
+ // 重置表单
535
+ newDatabase.value = {
536
+ name: '',
537
+ options: {
538
+ charset: '',
539
+ collation: '',
540
+ owner: '',
541
+ template: '',
542
+ encoding: '',
543
+ tablespace: ''
544
+ }
545
+ };
546
+ // 刷新数据库列表
547
+ await loadDatabases();
548
+ // 通知父组件刷新数据库缓存
549
+ emit('create-database');
550
+ } catch (error: any) {
551
+ modal.error(error.message || '创建数据库失败');
552
+ } finally {
553
+ creatingDatabase.value = false;
554
+ }
555
+ }
556
+ }
557
+
558
+ // 格式化文件大小
559
+ function formatFileSize(bytes: number): string {
560
+ if (bytes === 0) return '0 B';
561
+ const k = 1024;
562
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
563
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
564
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
565
+ }
566
+
567
+ // 格式化日期时间
568
+ function formatDate(dateString: string): string {
569
+ if (!dateString) return '';
570
+ const date = new Date(dateString);
571
+ return date.toLocaleString();
572
+ }
573
+
574
+ // 获取数据库类型标签
575
+ function getDbTypeLabel(type?: string): string {
576
+ const typeMap: Record<string, string> = {
577
+ mysql: 'MySQL',
578
+ postgres: 'PostgreSQL',
579
+ mssql: 'SQL Server',
580
+ sqlite: 'SQLite',
581
+ oracle: 'Oracle'
582
+ };
583
+ return typeMap[type || ''] || 'Unknown';
584
+ }
585
+
586
+ // 获取数据库Logo类名
587
+ function getDbLogoClass(type?: string): string {
588
+ return `db-${type || ''}`;
589
+ }
590
+
591
+ // 获取数据库Logo文本
592
+ function getDbLogoText(type?: string): string {
593
+ const textMap: Record<string, string> = {
594
+ mysql: 'MY',
595
+ postgres: 'PG',
596
+ mssql: 'MS',
597
+ sqlite: 'SQ',
598
+ oracle: 'OR'
599
+ };
600
+ return textMap[type || ''] || 'DB';
601
+ }
602
+ </script>
603
+
604
+ <style scoped>
605
+ .connection-detail {
606
+ padding: 20px;
607
+ display: flex;
608
+ flex-direction: column;
609
+ height: 100%;
610
+ gap: 20px;
611
+ }
612
+
613
+ .connection-header {
614
+ display: flex;
615
+ justify-content: space-between;
616
+ align-items: center;
617
+ margin-bottom: 20px;
618
+ padding: 20px;
619
+ background-color: #f8f9fa;
620
+ border-radius: 8px;
621
+ }
622
+
623
+ .connection-info {
624
+ display: flex;
625
+ align-items: center;
626
+ gap: 16px;
627
+ }
628
+
629
+ .connection-avatar {
630
+ width: 60px;
631
+ height: 60px;
632
+ display: flex;
633
+ align-items: center;
634
+ justify-content: center;
635
+ border-radius: 8px;
636
+ background-color: #e9ecef;
637
+ }
638
+
639
+ .db-logo {
640
+ font-size: 24px;
641
+ font-weight: bold;
642
+ color: #6c757d;
643
+ }
644
+
645
+ .db-mysql {
646
+ background-color: #4CAF50;
647
+ color: white;
648
+ }
649
+
650
+ .db-postgres {
651
+ background-color: #336791;
652
+ color: white;
653
+ }
654
+
655
+ .db-mssql {
656
+ background-color: #0078d4;
657
+ color: white;
658
+ }
659
+
660
+ .db-sqlite {
661
+ background-color: #003B57;
662
+ color: white;
663
+ }
664
+
665
+ .db-oracle {
666
+ background-color: #F80000;
667
+ color: white;
668
+ }
669
+
670
+ .connection-meta {
671
+ display: flex;
672
+ flex-direction: column;
673
+ gap: 4px;
674
+ }
675
+
676
+ .connection-name {
677
+ margin: 0;
678
+ font-size: 20px;
679
+ font-weight: 600;
680
+ }
681
+
682
+ .connection-type-info {
683
+ display: flex;
684
+ align-items: center;
685
+ gap: 12px;
686
+ }
687
+
688
+ .db-type {
689
+ padding: 4px 12px;
690
+ border-radius: 12px;
691
+ background-color: #e9ecef;
692
+ font-size: 12px;
693
+ font-weight: 500;
694
+ }
695
+
696
+ .connection-status {
697
+ display: flex;
698
+ align-items: center;
699
+ gap: 6px;
700
+ font-size: 14px;
701
+ }
702
+
703
+ .status-dot {
704
+ width: 8px;
705
+ height: 8px;
706
+ border-radius: 50%;
707
+ background-color: #6c757d;
708
+ }
709
+
710
+ .status-online .status-dot {
711
+ background-color: #28a745;
712
+ }
713
+
714
+ .status-offline .status-dot {
715
+ background-color: #dc3545;
716
+ }
717
+
718
+ .status-testing .status-dot {
719
+ background-color: #ffc107;
720
+ animation: pulse 1s infinite;
721
+ }
722
+
723
+ @keyframes pulse {
724
+ 0%, 100% {
725
+ opacity: 1;
726
+ }
727
+ 50% {
728
+ opacity: 0.5;
729
+ }
730
+ }
731
+
732
+ .connection-actions {
733
+ display: flex;
734
+ gap: 8px;
735
+ }
736
+
737
+ .connection-details-panel {
738
+ margin-bottom: 30px;
739
+ animation: fadeIn 0.3s ease;
740
+ }
741
+
742
+ .section-title {
743
+ margin: 0 0 16px 0;
744
+ font-size: 16px;
745
+ font-weight: 600;
746
+ display: flex;
747
+ align-items: center;
748
+ gap: 8px;
749
+ color: #343a40;
750
+ padding-bottom: 8px;
751
+ border-bottom: 1px solid #e9ecef;
752
+ }
753
+
754
+ @keyframes fadeIn {
755
+ from {
756
+ opacity: 0;
757
+ transform: translateY(-10px);
758
+ }
759
+ to {
760
+ opacity: 1;
761
+ transform: translateY(0);
762
+ }
763
+ }
764
+
765
+ .detail-card {
766
+ border: 1px solid #dee2e6;
767
+ border-radius: 8px;
768
+ overflow: hidden;
769
+ }
770
+
771
+ .card-header {
772
+ padding: 16px;
773
+ background-color: #f8f9fa;
774
+ border-bottom: 1px solid #dee2e6;
775
+ }
776
+
777
+ .card-title {
778
+ margin: 0;
779
+ font-size: 16px;
780
+ font-weight: 600;
781
+ display: flex;
782
+ align-items: center;
783
+ gap: 8px;
784
+ }
785
+
786
+ .card-body {
787
+ padding: 16px;
788
+ }
789
+
790
+ .info-grid {
791
+ display: grid;
792
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
793
+ gap: 16px;
794
+ margin-bottom: 10px;
795
+ }
796
+
797
+ .info-item {
798
+ display: flex;
799
+ flex-direction: column;
800
+ gap: 4px;
801
+ padding: 10px;
802
+ background-color: #f8f9fa;
803
+ border-radius: 6px;
804
+ transition: all 0.2s ease;
805
+ }
806
+
807
+ .info-item:hover {
808
+ background-color: #e9ecef;
809
+ transform: translateY(-1px);
810
+ }
811
+
812
+ .info-label {
813
+ font-size: 12px;
814
+ font-weight: 500;
815
+ color: #6c757d;
816
+ text-transform: uppercase;
817
+ letter-spacing: 0.5px;
818
+ }
819
+
820
+ .info-value {
821
+ font-size: 14px;
822
+ font-weight: 500;
823
+ color: #343a40;
824
+ }
825
+
826
+ .db-type-badge {
827
+ padding: 4px 10px;
828
+ border-radius: 12px;
829
+ font-size: 12px;
830
+ font-weight: 500;
831
+ }
832
+
833
+ .stats-grid {
834
+ display: grid;
835
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
836
+ gap: 16px;
837
+ }
838
+
839
+ .stat-item {
840
+ text-align: center;
841
+ padding: 12px;
842
+ background-color: #f8f9fa;
843
+ border-radius: 6px;
844
+ transition: all 0.2s ease;
845
+ }
846
+
847
+ .stat-item:hover {
848
+ background-color: #e9ecef;
849
+ transform: translateY(-1px);
850
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
851
+ }
852
+
853
+ .stat-value {
854
+ font-size: 20px;
855
+ font-weight: 600;
856
+ color: #343a40;
857
+ }
858
+
859
+ .stat-label {
860
+ font-size: 12px;
861
+ color: #6c757d;
862
+ margin-top: 4px;
863
+ }
864
+
865
+ .quick-actions {
866
+ margin-bottom: 30px;
867
+ padding: 20px;
868
+ background-color: #f8f9fa;
869
+ border-radius: 8px;
870
+ }
871
+
872
+ .actions-header {
873
+ margin-bottom: 16px;
874
+ }
875
+
876
+ .actions-title {
877
+ margin: 0;
878
+ font-size: 16px;
879
+ font-weight: 600;
880
+ display: flex;
881
+ align-items: center;
882
+ gap: 8px;
883
+ }
884
+
885
+ .actions-grid {
886
+ display: grid;
887
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
888
+ gap: 16px;
889
+ }
890
+
891
+ .action-btn {
892
+ display: flex;
893
+ flex-direction: column;
894
+ align-items: center;
895
+ gap: 8px;
896
+ padding: 16px;
897
+ background-color: white;
898
+ border: 1px solid #dee2e6;
899
+ border-radius: 8px;
900
+ cursor: pointer;
901
+ transition: all 0.2s ease;
902
+ }
903
+
904
+ .action-btn:hover {
905
+ background-color: #e9ecef;
906
+ transform: translateY(-2px);
907
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
908
+ }
909
+
910
+ .action-icon {
911
+ font-size: 24px;
912
+ color: #6c757d;
913
+ }
914
+
915
+ .action-text {
916
+ font-size: 14px;
917
+ font-weight: 500;
918
+ color: #343a40;
919
+ }
920
+
921
+ .connection-tabs {
922
+ border: 1px solid #dee2e6;
923
+ border-radius: 8px;
924
+ overflow: hidden;
925
+ flex: 1;
926
+ display: flex;
927
+ flex-direction: column;
928
+ }
929
+
930
+ .nav-tabs {
931
+ border-bottom: 1px solid #dee2e6;
932
+ background-color: #f8f9fa;
933
+ }
934
+
935
+ .nav-link {
936
+ border: none;
937
+ border-radius: 0;
938
+ padding: 12px 20px;
939
+ font-size: 14px;
940
+ font-weight: 500;
941
+ transition: all 0.2s ease;
942
+ }
943
+
944
+ .nav-link:hover {
945
+ background-color: #e9ecef;
946
+ }
947
+
948
+ .nav-link.active {
949
+ background-color: white;
950
+ border-bottom: 2px solid #0d6efd;
951
+ }
952
+
953
+ .tab-content {
954
+ flex: 1;
955
+ display: flex;
956
+ flex-direction: column;
957
+ overflow: hidden;
958
+ }
959
+
960
+ .tab-panel {
961
+ padding: 20px;
962
+ flex: 1;
963
+ display: flex;
964
+ flex-direction: column;
965
+ overflow: hidden;
966
+ }
967
+
968
+ .databases-section {
969
+ display: flex;
970
+ flex-direction: column;
971
+ gap: 16px;
972
+ flex: 1;
973
+ overflow: hidden;
974
+ }
975
+
976
+ .section-header {
977
+ display: flex;
978
+ justify-content: flex-end;
979
+ align-items: center;
980
+ }
981
+
982
+ .section-actions {
983
+ display: flex;
984
+ align-items: center;
985
+ gap: 8px;
986
+ }
987
+
988
+ .databases-list {
989
+ min-height: 300px;
990
+ height: 100%;
991
+ overflow-y: auto;
992
+ padding-right: 8px;
993
+ }
994
+
995
+ .loading-state {
996
+ display: flex;
997
+ flex-direction: column;
998
+ align-items: center;
999
+ justify-content: center;
1000
+ padding: 60px 20px;
1001
+ gap: 16px;
1002
+ }
1003
+
1004
+ .empty-state {
1005
+ display: flex;
1006
+ flex-direction: column;
1007
+ align-items: center;
1008
+ justify-content: center;
1009
+ padding: 60px 20px;
1010
+ gap: 16px;
1011
+ text-align: center;
1012
+ }
1013
+
1014
+ .empty-state i {
1015
+ font-size: 48px;
1016
+ color: #ced4da;
1017
+ }
1018
+
1019
+ .empty-state p {
1020
+ margin: 0;
1021
+ color: #6c757d;
1022
+ font-size: 16px;
1023
+ }
1024
+
1025
+ .databases-list-simple {
1026
+ display: grid;
1027
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1028
+ gap: 12px;
1029
+ }
1030
+
1031
+ .database-item {
1032
+ display: flex;
1033
+ flex-direction: column;
1034
+ align-items: center;
1035
+ gap: 8px;
1036
+ padding: 16px;
1037
+ border: 1px solid #dee2e6;
1038
+ border-radius: 6px;
1039
+ cursor: pointer;
1040
+ transition: all 0.2s ease;
1041
+ text-align: center;
1042
+ min-height: 100px;
1043
+ }
1044
+
1045
+ .database-item:hover {
1046
+ border-color: #0d6efd;
1047
+ background-color: #f8f9fa;
1048
+ transform: translateY(-2px);
1049
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1050
+ }
1051
+
1052
+ .database-item-icon {
1053
+ font-size: 32px;
1054
+ color: #6c757d;
1055
+ }
1056
+
1057
+ .database-item-name {
1058
+ font-size: 14px;
1059
+ font-weight: 500;
1060
+ color: #343a40;
1061
+ overflow: hidden;
1062
+ text-overflow: ellipsis;
1063
+ white-space: nowrap;
1064
+ max-width: 100%;
1065
+ }
1066
+
1067
+ .database-item-actions {
1068
+ display: flex;
1069
+ gap: 4px;
1070
+ margin-top: auto;
1071
+ }
1072
+
1073
+ .sql-section {
1074
+ display: flex;
1075
+ flex-direction: column;
1076
+ gap: 16px;
1077
+ flex: 1;
1078
+ overflow: hidden;
1079
+ }
1080
+
1081
+ .sql-header {
1082
+ display: flex;
1083
+ justify-content: space-between;
1084
+ align-items: center;
1085
+ }
1086
+
1087
+ .sql-title {
1088
+ margin: 0;
1089
+ font-size: 16px;
1090
+ font-weight: 600;
1091
+ display: flex;
1092
+ align-items: center;
1093
+ gap: 8px;
1094
+ }
1095
+
1096
+ .sql-db-info {
1097
+ display: flex;
1098
+ align-items: center;
1099
+ gap: 8px;
1100
+ }
1101
+
1102
+ /* 响应式设计 */
1103
+ @media (max-width: 768px) {
1104
+ .connection-header {
1105
+ flex-direction: column;
1106
+ align-items: flex-start;
1107
+ gap: 16px;
1108
+ }
1109
+
1110
+ .connection-actions {
1111
+ align-self: flex-end;
1112
+ }
1113
+
1114
+ .connection-cards {
1115
+ grid-template-columns: 1fr;
1116
+ }
1117
+
1118
+ .info-grid {
1119
+ grid-template-columns: 1fr;
1120
+ }
1121
+
1122
+ .stats-grid {
1123
+ grid-template-columns: repeat(2, 1fr);
1124
+ }
1125
+
1126
+ .actions-grid {
1127
+ grid-template-columns: repeat(2, 1fr);
1128
+ }
1129
+
1130
+ .databases-list-simple {
1131
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
1132
+ gap: 10px;
1133
+ }
1134
+
1135
+ .database-item {
1136
+ padding: 12px;
1137
+ min-height: 80px;
1138
+ }
1139
+
1140
+ .database-item-icon {
1141
+ font-size: 24px;
1142
+ }
1143
+
1144
+ .database-item-name {
1145
+ font-size: 12px;
1146
+ }
1147
+
1148
+ .nav-link {
1149
+ padding: 10px 16px;
1150
+ font-size: 13px;
1151
+ }
1152
+ }
1153
+ </style>
1154
+