fdb2 1.0.8 → 1.0.10

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 (235) hide show
  1. package/.dockerignore +21 -21
  2. package/.editorconfig +11 -11
  3. package/.eslintrc.cjs +14 -14
  4. package/.eslintrc.json +7 -7
  5. package/.prettierrc.js +3 -3
  6. package/.tpl.env +21 -21
  7. package/.vscodeignore +45 -45
  8. package/README.md +312 -312
  9. package/bin/build.sh +28 -28
  10. package/bin/deploy.sh +8 -8
  11. package/bin/dev.sh +10 -10
  12. package/bin/docker/dev-docker-compose.yml +43 -43
  13. package/bin/docker/dev.Dockerfile +24 -24
  14. package/bin/docker/prod-docker-compose.yml +17 -17
  15. package/bin/docker/prod.Dockerfile +29 -29
  16. package/bin/fdb2.js +220 -220
  17. package/dist/package.json +29 -29
  18. package/dist/pnpm-lock.yaml +1042 -354
  19. package/dist/public/explorer.css +1464 -1437
  20. package/dist/public/explorer.js +759 -223
  21. package/dist/public/index.css +1026 -1026
  22. package/dist/public/index.js +15 -9
  23. package/dist/public/layout.css +221 -221
  24. package/dist/public/layout.js +1 -1
  25. package/dist/public/vue.js +8 -2
  26. package/dist/scripts/preinstall.js +112 -112
  27. package/dist/server/index.d.ts.map +1 -1
  28. package/dist/server/index.js +8 -0
  29. package/dist/server/index.js.map +1 -1
  30. package/dist/server/index.ts +680 -671
  31. package/dist/server/model/connection.entity.ts +65 -65
  32. package/dist/server/model/database.entity.ts +245 -245
  33. package/dist/server/service/connection.service.d.ts +6 -1
  34. package/dist/server/service/connection.service.d.ts.map +1 -1
  35. package/dist/server/service/connection.service.js +18 -1
  36. package/dist/server/service/connection.service.js.map +1 -1
  37. package/dist/server/service/connection.service.ts +358 -341
  38. package/dist/server/service/database/base.service.d.ts +27 -0
  39. package/dist/server/service/database/base.service.d.ts.map +1 -1
  40. package/dist/server/service/database/base.service.js +17 -0
  41. package/dist/server/service/database/base.service.js.map +1 -1
  42. package/dist/server/service/database/base.service.ts +406 -367
  43. package/dist/server/service/database/cockroachdb.service.d.ts +16 -0
  44. package/dist/server/service/database/cockroachdb.service.d.ts.map +1 -1
  45. package/dist/server/service/database/cockroachdb.service.js +220 -154
  46. package/dist/server/service/database/cockroachdb.service.js.map +1 -1
  47. package/dist/server/service/database/cockroachdb.service.ts +871 -782
  48. package/dist/server/service/database/database.service.d.ts +4 -0
  49. package/dist/server/service/database/database.service.d.ts.map +1 -1
  50. package/dist/server/service/database/database.service.js +123 -0
  51. package/dist/server/service/database/database.service.js.map +1 -1
  52. package/dist/server/service/database/database.service.ts +775 -638
  53. package/dist/server/service/database/index.ts +6 -6
  54. package/dist/server/service/database/mongodb.service.d.ts +16 -0
  55. package/dist/server/service/database/mongodb.service.d.ts.map +1 -1
  56. package/dist/server/service/database/mongodb.service.js +35 -0
  57. package/dist/server/service/database/mongodb.service.js.map +1 -1
  58. package/dist/server/service/database/mongodb.service.ts +39 -1
  59. package/dist/server/service/database/mssql.service.d.ts +16 -0
  60. package/dist/server/service/database/mssql.service.d.ts.map +1 -1
  61. package/dist/server/service/database/mssql.service.js +168 -96
  62. package/dist/server/service/database/mssql.service.js.map +1 -1
  63. package/dist/server/service/database/mssql.service.ts +931 -840
  64. package/dist/server/service/database/mysql.service.d.ts +16 -0
  65. package/dist/server/service/database/mysql.service.d.ts.map +1 -1
  66. package/dist/server/service/database/mysql.service.js +189 -80
  67. package/dist/server/service/database/mysql.service.js.map +1 -1
  68. package/dist/server/service/database/mysql.service.ts +1025 -890
  69. package/dist/server/service/database/oracle.service.d.ts +16 -0
  70. package/dist/server/service/database/oracle.service.d.ts.map +1 -1
  71. package/dist/server/service/database/oracle.service.js +182 -120
  72. package/dist/server/service/database/oracle.service.js.map +1 -1
  73. package/dist/server/service/database/oracle.service.ts +1035 -959
  74. package/dist/server/service/database/postgres.service.d.ts +16 -0
  75. package/dist/server/service/database/postgres.service.d.ts.map +1 -1
  76. package/dist/server/service/database/postgres.service.js +154 -88
  77. package/dist/server/service/database/postgres.service.js.map +1 -1
  78. package/dist/server/service/database/postgres.service.ts +960 -871
  79. package/dist/server/service/database/sap.service.d.ts +16 -0
  80. package/dist/server/service/database/sap.service.d.ts.map +1 -1
  81. package/dist/server/service/database/sap.service.js +66 -0
  82. package/dist/server/service/database/sap.service.js.map +1 -1
  83. package/dist/server/service/database/sap.service.ts +89 -0
  84. package/dist/server/service/database/sqlite.service.d.ts +17 -1
  85. package/dist/server/service/database/sqlite.service.d.ts.map +1 -1
  86. package/dist/server/service/database/sqlite.service.js +78 -19
  87. package/dist/server/service/database/sqlite.service.js.map +1 -1
  88. package/dist/server/service/database/sqlite.service.ts +787 -708
  89. package/dist/server/service/session.service.ts +158 -158
  90. package/dist/view/index.html +38 -38
  91. package/env.d.ts +1 -1
  92. package/package.json +7 -2
  93. package/packages/vscode/.vscodeignore +44 -44
  94. package/packages/vscode/README.md +62 -62
  95. package/packages/vscode/out/database-services/cockroachdb.service.js +154 -154
  96. package/packages/vscode/out/database-services/mssql.service.js +96 -96
  97. package/packages/vscode/out/database-services/mysql.service.js +80 -80
  98. package/packages/vscode/out/database-services/oracle.service.js +120 -120
  99. package/packages/vscode/out/database-services/postgres.service.js +88 -88
  100. package/packages/vscode/out/database-services/sqlite.service.js +18 -18
  101. package/packages/vscode/out/provider/WebViewProvider.js +32 -32
  102. package/packages/vscode/package.json +142 -142
  103. package/packages/vscode/resources/icon.svg +5 -5
  104. package/packages/vscode/resources/webview/connection.css +41 -41
  105. package/packages/vscode/resources/webview/database.css +163 -163
  106. package/packages/vscode/resources/webview/index.html +9 -9
  107. package/packages/vscode/resources/webview/modules/header.tpl +13 -13
  108. package/packages/vscode/resources/webview/modules/initial_state.tpl +54 -54
  109. package/packages/vscode/resources/webview/query.css +104 -104
  110. package/packages/vscode/src/database-services/base.service.ts +362 -362
  111. package/packages/vscode/src/database-services/cockroachdb.service.ts +659 -659
  112. package/packages/vscode/src/database-services/connection.service.ts +340 -340
  113. package/packages/vscode/src/database-services/database.service.ts +629 -629
  114. package/packages/vscode/src/database-services/index.ts +6 -6
  115. package/packages/vscode/src/database-services/model/connection.entity.ts +65 -65
  116. package/packages/vscode/src/database-services/model/database.entity.ts +245 -245
  117. package/packages/vscode/src/database-services/mssql.service.ts +722 -722
  118. package/packages/vscode/src/database-services/mysql.service.ts +760 -760
  119. package/packages/vscode/src/database-services/oracle.service.ts +831 -831
  120. package/packages/vscode/src/database-services/postgres.service.ts +740 -740
  121. package/packages/vscode/src/database-services/sqlite.service.ts +558 -558
  122. package/packages/vscode/src/extension.ts +76 -76
  123. package/packages/vscode/src/provider/DatabaseTreeProvider.ts +167 -167
  124. package/packages/vscode/src/provider/WebViewProvider.ts +277 -277
  125. package/packages/vscode/src/service/DatabaseServiceBridge.ts +414 -414
  126. package/packages/vscode/src/typings/connection.ts +90 -90
  127. package/packages/vscode/tsconfig.json +21 -21
  128. package/public/index.html +9 -9
  129. package/public/modules/header.tpl +13 -13
  130. package/public/modules/initial_state.tpl +54 -54
  131. package/scripts/preinstall.js +112 -112
  132. package/server/index.ts +680 -671
  133. package/server/model/connection.entity.ts +65 -65
  134. package/server/model/database.entity.ts +245 -245
  135. package/server/service/connection.service.ts +358 -341
  136. package/server/service/database/base.service.ts +406 -367
  137. package/server/service/database/cockroachdb.service.ts +871 -782
  138. package/server/service/database/database.service.ts +775 -638
  139. package/server/service/database/index.ts +6 -6
  140. package/server/service/database/mongodb.service.ts +39 -1
  141. package/server/service/database/mssql.service.ts +931 -840
  142. package/server/service/database/mysql.service.ts +1025 -890
  143. package/server/service/database/oracle.service.ts +1035 -959
  144. package/server/service/database/postgres.service.ts +960 -871
  145. package/server/service/database/sap.service.ts +89 -0
  146. package/server/service/database/sqlite.service.ts +787 -708
  147. package/server/service/session.service.ts +158 -158
  148. package/server/tsconfig.json +20 -20
  149. package/server.js +149 -149
  150. package/server.pid +1 -0
  151. package/src/adapter/ajax.ts +135 -135
  152. package/src/assets/base.css +1 -1
  153. package/src/assets/database.css +949 -949
  154. package/src/assets/images/svg/illustrations/illustration-1.svg +1 -1
  155. package/src/assets/images/svg/illustrations/illustration-2.svg +2 -2
  156. package/src/assets/images/svg/illustrations/illustration-3.svg +50 -50
  157. package/src/assets/images/svg/illustrations/illustration-4.svg +1 -1
  158. package/src/assets/images/svg/illustrations/illustration-5.svg +73 -73
  159. package/src/assets/images/svg/illustrations/illustration-6.svg +89 -89
  160. package/src/assets/images/svg/illustrations/illustration-7.svg +39 -39
  161. package/src/assets/images/svg/separators/curve-2.svg +3 -3
  162. package/src/assets/images/svg/separators/curve.svg +3 -3
  163. package/src/assets/images/svg/separators/line.svg +3 -3
  164. package/src/assets/logo.svg +73 -73
  165. package/src/assets/main.css +1 -1
  166. package/src/base/config.ts +20 -20
  167. package/src/base/detect.ts +134 -134
  168. package/src/base/entity.ts +92 -92
  169. package/src/base/eventBus.ts +36 -36
  170. package/src/components/connection-editor/index.vue +588 -588
  171. package/src/components/dataGrid/index.vue +104 -104
  172. package/src/components/dataGrid/pagination.vue +105 -105
  173. package/src/components/loading/index.vue +42 -42
  174. package/src/components/modal/index.ts +180 -180
  175. package/src/components/modal/index.vue +560 -560
  176. package/src/components/toast/index.ts +43 -43
  177. package/src/components/toast/toast.vue +57 -57
  178. package/src/components/user/name.vue +103 -103
  179. package/src/components/user/selector.vue +416 -416
  180. package/src/domain/SysConfig.ts +74 -74
  181. package/src/platform/App.vue +7 -7
  182. package/src/platform/database/components/connection-detail.vue +1153 -1154
  183. package/src/platform/database/components/data-editor.vue +477 -477
  184. package/src/platform/database/components/database-detail.vue +1173 -1172
  185. package/src/platform/database/components/database-monitor.vue +1085 -1085
  186. package/src/platform/database/components/db-tools.vue +1264 -816
  187. package/src/platform/database/components/query-history.vue +1348 -1348
  188. package/src/platform/database/components/sql-executor.vue +737 -737
  189. package/src/platform/database/components/sql-query-editor.vue +1045 -1045
  190. package/src/platform/database/components/table-detail.vue +1375 -1376
  191. package/src/platform/database/components/table-editor.vue +916 -916
  192. package/src/platform/database/explorer.vue +1839 -1839
  193. package/src/platform/database/index.vue +1192 -1192
  194. package/src/platform/database/layout.vue +366 -366
  195. package/src/platform/database/router.ts +36 -36
  196. package/src/platform/database/styles/common.scss +601 -601
  197. package/src/platform/database/types/common.ts +444 -444
  198. package/src/platform/database/utils/export.ts +231 -231
  199. package/src/platform/database/utils/helpers.ts +436 -436
  200. package/src/platform/index.ts +32 -32
  201. package/src/platform/router.ts +40 -40
  202. package/src/platform/vscode/bridge.ts +121 -121
  203. package/src/platform/vscode/components/ConnectionPanel.vue +272 -272
  204. package/src/platform/vscode/components/DatabasePanel.vue +532 -532
  205. package/src/platform/vscode/components/QueryPanel.vue +371 -371
  206. package/src/platform/vscode/entry/connection.ts +13 -13
  207. package/src/platform/vscode/entry/database.ts +13 -13
  208. package/src/platform/vscode/entry/query.ts +13 -13
  209. package/src/platform/vscode/index.ts +5 -5
  210. package/src/service/base.ts +133 -127
  211. package/src/service/database.ts +505 -495
  212. package/src/service/login.ts +120 -120
  213. package/src/shims-vue.d.ts +6 -6
  214. package/src/stores/connection.ts +266 -266
  215. package/src/stores/session.ts +87 -87
  216. package/src/typings/database-types.ts +412 -412
  217. package/src/typings/database.ts +363 -363
  218. package/src/typings/global.d.ts +58 -58
  219. package/src/typings/pinia.d.ts +7 -7
  220. package/src/utils/clipboard.ts +29 -29
  221. package/src/utils/database-types.ts +242 -242
  222. package/src/utils/modal.ts +123 -123
  223. package/src/utils/request.ts +55 -55
  224. package/src/utils/sleep.ts +3 -3
  225. package/src/utils/toast.ts +73 -73
  226. package/src/utils/util.ts +171 -171
  227. package/src/utils/xlsx.ts +228 -228
  228. package/tsconfig.json +33 -33
  229. package/view/index.html +9 -9
  230. package/view/modules/header.tpl +13 -13
  231. package/view/modules/initial_state.tpl +19 -19
  232. package/vite.config.ts +424 -424
  233. package/vite.config.vscode.ts +47 -47
  234. package/fdb2.server.pid +0 -1
  235. package/server/backups/db_ai_breakout_2026-03-11T08-38-48-677Z.sql +0 -0
@@ -1,1154 +1,1153 @@
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
-
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
+ flex: 1;
925
+ display: flex;
926
+ flex-direction: column;
927
+ }
928
+
929
+ .nav-tabs {
930
+ border-bottom: 1px solid #dee2e6;
931
+ background-color: #f8f9fa;
932
+ }
933
+
934
+ .nav-link {
935
+ border: none;
936
+ border-radius: 0;
937
+ padding: 12px 20px;
938
+ font-size: 14px;
939
+ font-weight: 500;
940
+ transition: all 0.2s ease;
941
+ }
942
+
943
+ .nav-link:hover {
944
+ background-color: #e9ecef;
945
+ }
946
+
947
+ .nav-link.active {
948
+ background-color: white;
949
+ border-bottom: 2px solid #0d6efd;
950
+ }
951
+
952
+ .tab-content {
953
+ flex: 1;
954
+ display: flex;
955
+ flex-direction: column;
956
+ overflow: hidden;
957
+ }
958
+
959
+ .tab-panel {
960
+ padding: 20px;
961
+ flex: 1;
962
+ display: flex;
963
+ flex-direction: column;
964
+ overflow: hidden;
965
+ }
966
+
967
+ .databases-section {
968
+ display: flex;
969
+ flex-direction: column;
970
+ gap: 16px;
971
+ flex: 1;
972
+ overflow: hidden;
973
+ }
974
+
975
+ .section-header {
976
+ display: flex;
977
+ justify-content: flex-end;
978
+ align-items: center;
979
+ }
980
+
981
+ .section-actions {
982
+ display: flex;
983
+ align-items: center;
984
+ gap: 8px;
985
+ }
986
+
987
+ .databases-list {
988
+ min-height: 300px;
989
+ height: 100%;
990
+ overflow-y: auto;
991
+ padding-right: 8px;
992
+ }
993
+
994
+ .loading-state {
995
+ display: flex;
996
+ flex-direction: column;
997
+ align-items: center;
998
+ justify-content: center;
999
+ padding: 60px 20px;
1000
+ gap: 16px;
1001
+ }
1002
+
1003
+ .empty-state {
1004
+ display: flex;
1005
+ flex-direction: column;
1006
+ align-items: center;
1007
+ justify-content: center;
1008
+ padding: 60px 20px;
1009
+ gap: 16px;
1010
+ text-align: center;
1011
+ }
1012
+
1013
+ .empty-state i {
1014
+ font-size: 48px;
1015
+ color: #ced4da;
1016
+ }
1017
+
1018
+ .empty-state p {
1019
+ margin: 0;
1020
+ color: #6c757d;
1021
+ font-size: 16px;
1022
+ }
1023
+
1024
+ .databases-list-simple {
1025
+ display: grid;
1026
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1027
+ gap: 12px;
1028
+ }
1029
+
1030
+ .database-item {
1031
+ display: flex;
1032
+ flex-direction: column;
1033
+ align-items: center;
1034
+ gap: 8px;
1035
+ padding: 16px;
1036
+ border: 1px solid #dee2e6;
1037
+ border-radius: 6px;
1038
+ cursor: pointer;
1039
+ transition: all 0.2s ease;
1040
+ text-align: center;
1041
+ min-height: 100px;
1042
+ }
1043
+
1044
+ .database-item:hover {
1045
+ border-color: #0d6efd;
1046
+ background-color: #f8f9fa;
1047
+ transform: translateY(-2px);
1048
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1049
+ }
1050
+
1051
+ .database-item-icon {
1052
+ font-size: 32px;
1053
+ color: #6c757d;
1054
+ }
1055
+
1056
+ .database-item-name {
1057
+ font-size: 14px;
1058
+ font-weight: 500;
1059
+ color: #343a40;
1060
+ overflow: hidden;
1061
+ text-overflow: ellipsis;
1062
+ white-space: nowrap;
1063
+ max-width: 100%;
1064
+ }
1065
+
1066
+ .database-item-actions {
1067
+ display: flex;
1068
+ gap: 4px;
1069
+ margin-top: auto;
1070
+ }
1071
+
1072
+ .sql-section {
1073
+ display: flex;
1074
+ flex-direction: column;
1075
+ gap: 16px;
1076
+ flex: 1;
1077
+ overflow: hidden;
1078
+ }
1079
+
1080
+ .sql-header {
1081
+ display: flex;
1082
+ justify-content: space-between;
1083
+ align-items: center;
1084
+ }
1085
+
1086
+ .sql-title {
1087
+ margin: 0;
1088
+ font-size: 16px;
1089
+ font-weight: 600;
1090
+ display: flex;
1091
+ align-items: center;
1092
+ gap: 8px;
1093
+ }
1094
+
1095
+ .sql-db-info {
1096
+ display: flex;
1097
+ align-items: center;
1098
+ gap: 8px;
1099
+ }
1100
+
1101
+ /* 响应式设计 */
1102
+ @media (max-width: 768px) {
1103
+ .connection-header {
1104
+ flex-direction: column;
1105
+ align-items: flex-start;
1106
+ gap: 16px;
1107
+ }
1108
+
1109
+ .connection-actions {
1110
+ align-self: flex-end;
1111
+ }
1112
+
1113
+ .connection-cards {
1114
+ grid-template-columns: 1fr;
1115
+ }
1116
+
1117
+ .info-grid {
1118
+ grid-template-columns: 1fr;
1119
+ }
1120
+
1121
+ .stats-grid {
1122
+ grid-template-columns: repeat(2, 1fr);
1123
+ }
1124
+
1125
+ .actions-grid {
1126
+ grid-template-columns: repeat(2, 1fr);
1127
+ }
1128
+
1129
+ .databases-list-simple {
1130
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
1131
+ gap: 10px;
1132
+ }
1133
+
1134
+ .database-item {
1135
+ padding: 12px;
1136
+ min-height: 80px;
1137
+ }
1138
+
1139
+ .database-item-icon {
1140
+ font-size: 24px;
1141
+ }
1142
+
1143
+ .database-item-name {
1144
+ font-size: 12px;
1145
+ }
1146
+
1147
+ .nav-link {
1148
+ padding: 10px 16px;
1149
+ font-size: 13px;
1150
+ }
1151
+ }
1152
+ </style>
1153
+