fdb2 1.0.8 → 1.0.9

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 +15 -0
  36. package/dist/server/service/connection.service.js.map +1 -1
  37. package/dist/server/service/connection.service.ts +356 -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 +16 -0
  85. package/dist/server/service/database/sqlite.service.d.ts.map +1 -1
  86. package/dist/server/service/database/sqlite.service.js +77 -18
  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 +1 -1
  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 +356 -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,817 +1,1265 @@
1
- <template>
2
- <div class="db-tools">
3
- <div class="tools-header">
4
- <h5 class="tools-title">
5
- <i class="bi bi-tools"></i>
6
- 数据库管理工具
7
- </h5>
8
- </div>
9
-
10
- <div class="tools-content">
11
- <!-- 数据备份 -->
12
- <div class="tool-section">
13
- <h6 class="section-title">
14
- <i class="bi bi-shield-check"></i>
15
- 数据备份
16
- </h6>
17
- <div class="tool-actions">
18
- <button class="btn btn-outline-primary btn-sm" @click="backupDatabase">
19
- <i class="bi bi-download"></i> 备份数据库
20
- </button>
21
- <button class="btn btn-outline-secondary btn-sm" @click="showRestoreModal">
22
- <i class="bi bi-upload"></i> 恢复数据库
23
- </button>
24
- <button class="btn btn-outline-info btn-sm" @click="showScheduleModal">
25
- <i class="bi bi-clock"></i> 定时备份
26
- </button>
27
- </div>
28
- </div>
29
-
30
- <!-- 用户管理 -->
31
- <div class="tool-section">
32
- <h6 class="section-title">
33
- <i class="bi bi-people"></i>
34
- 用户管理
35
- </h6>
36
- <div class="tool-actions">
37
- <button class="btn btn-outline-success btn-sm" @click="showUsersList">
38
- <i class="bi bi-person-lines-fill"></i> 用户列表
39
- </button>
40
- <button class="btn btn-outline-primary btn-sm" @click="showCreateUserModal">
41
- <i class="bi bi-person-plus"></i> 创建用户
42
- </button>
43
- <button class="btn btn-outline-warning btn-sm" @click="showPermissionsModal">
44
- <i class="bi bi-key"></i> 权限管理
45
- </button>
46
- </div>
47
- </div>
48
-
49
- <!-- 性能监控 -->
50
- <div class="tool-section">
51
- <h6 class="section-title">
52
- <i class="bi bi-speedometer2"></i>
53
- 性能监控
54
- </h6>
55
- <div class="tool-actions">
56
- <button class="btn btn-outline-info btn-sm" @click="showProcessList">
57
- <i class="bi bi-activity"></i> 进程列表
58
- </button>
59
- <button class="btn btn-outline-warning btn-sm" @click="showSlowQueries">
60
- <i class="bi bi-hourglass-split"></i> 慢查询
61
- </button>
62
- <button class="btn btn-outline-danger btn-sm" @click="showConnectionsList">
63
- <i class="bi bi-diagram-3"></i> 连接数
64
- </button>
65
- </div>
66
- </div>
67
-
68
- <!-- 数据库优化 -->
69
- <div class="tool-section">
70
- <h6 class="section-title">
71
- <i class="bi bi-gear-wide-connected"></i>
72
- 数据库优化
73
- </h6>
74
- <div class="tool-actions">
75
- <button class="btn btn-outline-success btn-sm" @click="optimizeDatabase">
76
- <i class="bi bi-lightning-charge"></i> 优化数据库
77
- </button>
78
- <button class="btn btn-outline-primary btn-sm" @click="analyzeTables">
79
- <i class="bi bi-search"></i> 分析表
80
- </button>
81
- <button class="btn btn-outline-secondary btn-sm" @click="repairTables">
82
- <i class="bi bi-tools"></i> 修复表
83
- </button>
84
- <button class="btn btn-outline-info btn-sm" @click="clearLogs">
85
- <i class="bi bi-trash"></i> 清理日志
86
- </button>
87
- </div>
88
- </div>
89
-
90
- <!-- 数据迁移 -->
91
- <div class="tool-section">
92
- <h6 class="section-title">
93
- <i class="bi bi-arrow-left-right"></i>
94
- 数据迁移
95
- </h6>
96
- <div class="tool-actions">
97
- <button class="btn btn-outline-primary btn-sm" @click="showExportModal">
98
- <i class="bi bi-box-arrow-up-right"></i> 导出结构
99
- </button>
100
- <button class="btn btn-outline-success btn-sm" @click="showImportModal">
101
- <i class="bi bi-box-arrow-in-down"></i> 导入数据
102
- </button>
103
- <button class="btn btn-outline-warning btn-sm" @click="showSyncModal">
104
- <i class="bi bi-arrow-repeat"></i> 数据同步
105
- </button>
106
- </div>
107
- </div>
108
-
109
- <!-- 健康检查 -->
110
- <div class="tool-section">
111
- <h6 class="section-title">
112
- <i class="bi bi-heart-pulse"></i>
113
- 健康检查
114
- </h6>
115
- <div class="tool-actions">
116
- <button class="btn btn-outline-info btn-sm" @click="runHealthCheck">
117
- <i class="bi bi-clipboard-check"></i> 健康检查
118
- </button>
119
- <button class="btn btn-outline-secondary btn-sm" @click="showStatistics">
120
- <i class="bi bi-bar-chart"></i> 数据统计
121
- </button>
122
- <button class="btn btn-outline-warning btn-sm" @click="showAuditLog">
123
- <i class="bi bi-journal-text"></i> 审计日志
124
- </button>
125
- </div>
126
- </div>
127
- </div>
128
-
129
- <!-- 执行结果展示区域 -->
130
- <div class="execution-results">
131
- <div class="results-header">
132
- <h6 class="results-title">
133
- <i class="bi bi-terminal"></i>
134
- 执行结果
135
- </h6>
136
- <button class="btn btn-outline-secondary btn-sm" @click="clearResults">
137
- <i class="bi bi-trash"></i> 清空
138
- </button>
139
- </div>
140
- <div class="results-content" ref="resultsContentRef">
141
- <div v-if="executionResults.length === 0" class="no-results">
142
- <i class="bi bi-inbox"></i>
143
- <p>暂无执行结果</p>
144
- </div>
145
- <div v-for="(result, index) in executionResults" :key="index" class="result-item" :class="`result-${result.status}`">
146
- <div class="result-header" @click="toggleResult(index)">
147
- <div class="result-title">
148
- <i :class="getResultIcon(result.status)"></i>
149
- <span class="operation-name">{{ result.operation }}</span>
150
- <span class="operation-time">{{ result.timestamp }}</span>
151
- </div>
152
- <i class="bi bi-chevron-down toggle-icon" :class="{ 'expanded': result.expanded }"></i>
153
- </div>
154
- <div v-if="result.expanded" class="result-body">
155
- <pre><code v-html="highlightJson(result.data)"></code></pre>
156
- </div>
157
- </div>
158
- </div>
159
- </div>
160
-
161
- <!-- 数据恢复模态框 -->
162
- <div class="modal fade" :class="{ show: restoreModalVisible }" :style="{ display: restoreModalVisible ? 'block' : 'none', zIndex: 1055 }">
163
- <div class="modal-dialog">
164
- <div class="modal-content">
165
- <div class="modal-header">
166
- <h5 class="modal-title">恢复数据库</h5>
167
- <button type="button" class="btn-close" @click="closeRestoreModal"></button>
168
- </div>
169
- <div class="modal-body">
170
- <p>请选择要恢复的备份文件:</p>
171
- <div class="mb-3">
172
- <input type="file" class="form-control" @change="handleFileChange" accept=".sql,.bak">
173
- </div>
174
- <div v-if="selectedFile" class="alert alert-info">
175
- 已选择文件:{{ selectedFile.name }}
176
- </div>
177
- <div class="mb-3 form-check">
178
- <input type="checkbox" class="form-check-input" v-model="restoreOptions.dropExisting" id="dropExisting">
179
- <label class="form-check-label" for="dropExisting">删除现有表</label>
180
- </div>
181
- </div>
182
- <div class="modal-footer">
183
- <button type="button" class="btn btn-secondary" @click="closeRestoreModal">取消</button>
184
- <button type="button" class="btn btn-primary" @click="performRestore" :disabled="!selectedFile">
185
- <span v-if="restoring" class="spinner-border spinner-border-sm me-2"></span>
186
- 恢复
187
- </button>
188
- </div>
189
- </div>
190
- </div>
191
- </div>
192
- </div>
193
- </template>
194
-
195
- <script lang="ts" setup>
196
- import { ref } from 'vue';
197
- import { DatabaseService } from '@/service/database';
198
- import { modal } from '@/utils/modal';
199
-
200
- const props = defineProps<{
201
- connection: any;
202
- database: string;
203
- }>();
204
-
205
- const emit = defineEmits<{
206
- 'execute-sql': [sql: string];
207
- }>();
208
-
209
- const databaseService = new DatabaseService();
210
-
211
- // 状态管理
212
- const restoreModalVisible = ref(false);
213
- const selectedFile = ref<File | null>(null);
214
- const restoring = ref(false);
215
- const resultsContentRef = ref<HTMLElement | null>(null);
216
-
217
- // 执行结果历史
218
- interface ExecutionResult {
219
- operation: string;
220
- status: 'success' | 'error' | 'info';
221
- timestamp: string;
222
- data: any;
223
- expanded: boolean;
224
- }
225
-
226
- const executionResults = ref<ExecutionResult[]>([]);
227
-
228
- const restoreOptions = ref({
229
- dropExisting: false
230
- });
231
-
232
- // 添加执行结果
233
- function addExecutionResult(operation: string, status: 'success' | 'error' | 'info', data: any) {
234
- const timestamp = new Date().toLocaleString('zh-CN', {
235
- year: 'numeric',
236
- month: '2-digit',
237
- day: '2-digit',
238
- hour: '2-digit',
239
- minute: '2-digit',
240
- second: '2-digit'
241
- });
242
-
243
- executionResults.value.unshift({
244
- operation,
245
- status,
246
- timestamp,
247
- data,
248
- expanded: false
249
- });
250
-
251
- // 只保留最近50条记录
252
- if (executionResults.value.length > 50) {
253
- executionResults.value = executionResults.value.slice(0, 50);
254
- }
255
-
256
- // 自动滚动到底部(显示最新结果在顶部,所以滚动到0)
257
- setTimeout(() => {
258
- if (resultsContentRef.value) {
259
- resultsContentRef.value.scrollTop = 0;
260
- }
261
- }, 100);
262
- }
263
-
264
- // 清空执行结果
265
- function clearResults() {
266
- executionResults.value = [];
267
- }
268
-
269
- // 切换结果展开/收起
270
- function toggleResult(index: number) {
271
- const result = executionResults.value[index];
272
- if (result) {
273
- result.expanded = !result.expanded;
274
- }
275
- }
276
-
277
- // 格式化错误信息
278
- function formatError(error: any): any {
279
- const formatted: any = {
280
- success: false,
281
- message: error.msg || error.message || '未知错误'
282
- };
283
- if (error.stack) {
284
- formatted.stack = error.stack;
285
- }
286
- return formatted;
287
- }
288
-
289
- // JSON 语法高亮
290
- function highlightJson(data: any): string {
291
- if (data === null || data === undefined) return '';
292
- const jsonStr = JSON.stringify(data, null, 2);
293
- return jsonStr.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
294
- let cls = 'json-number';
295
- if (/^"/.test(match)) {
296
- if (/:$/.test(match)) {
297
- cls = 'json-key';
298
- } else {
299
- cls = 'json-string';
300
- }
301
- } else if (/true|false/.test(match)) {
302
- cls = 'json-boolean';
303
- } else if (/null/.test(match)) {
304
- cls = 'json-null';
305
- }
306
- return '<span class="' + cls + '">' + match + '</span>';
307
- });
308
- }
309
-
310
- // 获取结果图标
311
- function getResultIcon(status: string): string {
312
- switch (status) {
313
- case 'success':
314
- return 'bi bi-check-circle-fill text-success';
315
- case 'error':
316
- return 'bi bi-x-circle-fill text-danger';
317
- case 'info':
318
- return 'bi bi-info-circle-fill text-info';
319
- default:
320
- return 'bi bi-dash-circle-fill text-secondary';
321
- }
322
- }
323
-
324
- // 数据备份
325
- async function backupDatabase() {
326
- const operation = '备份数据库';
327
- try {
328
- const res = await databaseService.backupDatabase(props.connection?.id || '', props.database);
329
- if(res.ret === 0) {
330
- addExecutionResult(operation, 'success', res);
331
- } else {
332
- modal.error(res.msg || '备份失败');
333
- addExecutionResult(operation, 'error', formatError(res));
334
- }
335
- } catch (error: any) {
336
- console.error('备份失败:', error);
337
- modal.error(error.msg || error.message || '备份失败');
338
- addExecutionResult(operation, 'error', formatError(error));
339
- }
340
- }
341
-
342
- // 用户管理
343
- function showUsersList() {
344
- addExecutionResult('用户列表', 'info', { message: '用户列表功能开发中...' });
345
- }
346
-
347
- function showCreateUserModal() {
348
- addExecutionResult('创建用户', 'info', { message: '创建用户功能开发中...' });
349
- }
350
-
351
- function showPermissionsModal() {
352
- addExecutionResult('权限管理', 'info', { message: '权限管理功能开发中...' });
353
- }
354
-
355
- // 性能监控
356
- function showProcessList() {
357
- const sql = 'SHOW PROCESSLIST';
358
- addExecutionResult('进程列表', 'info', { sql: sql, message: '已发送 SQL 查询' });
359
- emit('execute-sql', sql);
360
- }
361
-
362
- function showSlowQueries() {
363
- const sql = 'SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10';
364
- addExecutionResult('慢查询', 'info', { sql: sql, message: '已发送 SQL 查询' });
365
- emit('execute-sql', sql);
366
- }
367
-
368
- function showConnectionsList() {
369
- const sql = 'SHOW STATUS LIKE "Threads_connected"';
370
- addExecutionResult('连接数', 'info', { sql: sql, message: '已发送 SQL 查询' });
371
- emit('execute-sql', sql);
372
- }
373
-
374
- // 数据库优化
375
- async function optimizeDatabase() {
376
- const operation = '优化数据库';
377
- try {
378
- const res = await databaseService.optimizeDatabase(props.connection?.id || '', props.database);
379
- if(res.ret === 0) {
380
- addExecutionResult(operation, 'success', res.data);
381
- } else {
382
- modal.error(res.msg || '优化失败');
383
- addExecutionResult(operation, 'error', formatError(res));
384
- }
385
- } catch (error: any) {
386
- console.error('优化失败:', error);
387
- modal.error(error.msg || error.message || '优化失败');
388
- addExecutionResult(operation, 'error', formatError(error));
389
- }
390
- }
391
-
392
- async function analyzeTables() {
393
- const operation = '分析表';
394
- try {
395
- const res = await databaseService.analyzeTables(props.connection?.id || '', props.database);
396
- if(res.ret === 0) {
397
- addExecutionResult(operation, 'success', res.data);
398
- } else {
399
- modal.error(res.msg || '分析失败');
400
- addExecutionResult(operation, 'error', formatError(res));
401
- }
402
- } catch (error: any) {
403
- console.error('分析失败:', error);
404
- modal.error(res.msg || error.message || '分析失败');
405
- addExecutionResult(operation, 'error', formatError(error));
406
- }
407
- }
408
-
409
- async function repairTables() {
410
- const operation = '修复表';
411
- try {
412
- const res = await databaseService.repairTables(props.connection?.id || '', props.database);
413
- if(res.ret === 0) {
414
- addExecutionResult(operation, 'success', res.data);
415
- } else {
416
- modal.error(res.msg || '修复失败');
417
- addExecutionResult(operation, 'error', formatError(res));
418
- }
419
- } catch (error: any) {
420
- console.error('修复失败:', error);
421
- modal.error(res.msg || error.message || '修复失败');
422
- addExecutionResult(operation, 'error', formatError(error));
423
- }
424
- }
425
-
426
- async function clearLogs() {
427
- const operation = '清理日志';
428
- const logs = [
429
- 'TRUNCATE TABLE mysql.slow_log',
430
- 'TRUNCATE TABLE mysql.general_log',
431
- 'FLUSH LOGS'
432
- ];
433
-
434
- logs.forEach(sql => {
435
- addExecutionResult(`清理日志 - ${sql.split(' ')[1]}`, 'info', { sql, message: '已发送 SQL 查询' });
436
- emit('execute-sql', sql);
437
- });
438
- }
439
-
440
- // 数据迁移
441
- function showExportModal() {
442
- addExecutionResult('导出结构', 'info', { message: '导出结构功能开发中...' });
443
- }
444
-
445
- function showImportModal() {
446
- addExecutionResult('导入数据', 'info', { message: '导入数据功能开发中...' });
447
- }
448
-
449
- function showSyncModal() {
450
- addExecutionResult('数据同步', 'info', { message: '数据同步功能开发中...' });
451
- }
452
-
453
- // 健康检查
454
- async function runHealthCheck() {
455
- const operation = '健康检查';
456
- const checks = [
457
- { name: '连接状态', sql: 'SELECT 1 as status' },
458
- { name: '表完整性', sql: 'SELECT COUNT(*) as status FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = "BASE TABLE"' },
459
- { name: '索引状态', sql: 'SELECT COUNT(*) as status FROM information_schema.statistics WHERE table_schema = DATABASE()' },
460
- { name: '磁盘空间', sql: 'SELECT SUM(data_length + index_length) as status FROM information_schema.tables WHERE table_schema = DATABASE()' }
461
- ];
462
-
463
- const results: any[] = [];
464
- for (const check of checks) {
465
- try {
466
- // 这里应该调用实际的数据库查询
467
- results.push({
468
- name: check.name,
469
- status: 'healthy',
470
- message: '正常'
471
- });
472
- } catch (error: any) {
473
- results.push({
474
- name: check.name,
475
- status: 'error',
476
- message: error.message
477
- });
478
- }
479
- }
480
-
481
- addExecutionResult(operation, 'success', { checks: results });
482
- }
483
-
484
- function showStatistics() {
485
- const sql = `
486
- SELECT
487
- table_name as '表名',
488
- table_rows as '记录数',
489
- ROUND(((data_length + index_length) / 1024 / 1024), 2) as '大小(MB)'
490
- FROM information_schema.tables
491
- WHERE table_schema = DATABASE()
492
- ORDER BY (data_length + index_length) DESC
493
- `;
494
- addExecutionResult('数据统计', 'info', { sql: sql, message: '已发送 SQL 查询' });
495
- emit('execute-sql', sql);
496
- }
497
-
498
- function showAuditLog() {
499
- const sql = 'SELECT * FROM mysql.general_log ORDER BY event_time DESC LIMIT 100';
500
- addExecutionResult('审计日志', 'info', { sql: sql, message: '已发送 SQL 查询' });
501
- emit('execute-sql', sql);
502
- }
503
-
504
- // 恢复功能
505
- function showRestoreModal() {
506
- restoreModalVisible.value = true;
507
- }
508
-
509
- function closeRestoreModal() {
510
- restoreModalVisible.value = false;
511
- selectedFile.value = null;
512
- }
513
-
514
- function handleFileSelect(event: Event) {
515
- const target = event.target as HTMLInputElement;
516
- if (target.files && target.files.length > 0) {
517
- selectedFile.value = target.files[0] as File;
518
- }
519
- }
520
-
521
- async function performRestore() {
522
- if (!selectedFile.value) return;
523
-
524
- const operation = '恢复数据库';
525
- try {
526
- restoring.value = true;
527
- const filePath = selectedFile.value.name;
528
-
529
- const res = await databaseService.restoreDatabase(
530
- props.connection?.id || '',
531
- props.database,
532
- filePath,
533
- { dropExisting: restoreOptions.value.dropExisting }
534
- );
535
-
536
- addExecutionResult(operation, 'success', res);
537
- closeRestoreModal();
538
- } catch (error: any) {
539
- console.error('恢复失败:', error);
540
- modal.error(error.msg || error.message || '恢复失败');
541
- addExecutionResult(operation, 'error', formatError(error));
542
- } finally {
543
- restoring.value = false;
544
- }
545
- }
546
-
547
- function showScheduleModal() {
548
- addExecutionResult('定时备份', 'info', { message: '定时备份功能开发中...' });
549
- }
550
- </script>
551
-
552
- <style scoped>
553
- .db-tools {
554
- background: white;
555
- border-radius: 12px;
556
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
557
- overflow: hidden;
558
- }
559
-
560
- .tools-header {
561
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
562
- color: white;
563
- padding: 1rem 1.5rem;
564
- border-bottom: 1px solid #e2e8f0;
565
- }
566
-
567
- .tools-title {
568
- margin: 0;
569
- font-size: 1.1rem;
570
- font-weight: 600;
571
- display: flex;
572
- align-items: center;
573
- gap: 0.5rem;
574
- }
575
-
576
- .tools-content {
577
- padding: 1.5rem;
578
- max-height: 500px;
579
- overflow-y: auto;
580
- }
581
-
582
- .tool-section {
583
- margin-bottom: 2rem;
584
- }
585
-
586
- .section-title {
587
- font-size: 0.9rem;
588
- font-weight: 600;
589
- color: #374151;
590
- margin-bottom: 1rem;
591
- display: flex;
592
- align-items: center;
593
- gap: 0.5rem;
594
- }
595
-
596
- .tool-actions {
597
- display: flex;
598
- flex-wrap: wrap;
599
- gap: 0.5rem;
600
- }
601
-
602
- .tool-actions .btn {
603
- min-width: 120px;
604
- display: flex;
605
- align-items: center;
606
- gap: 0.5rem;
607
- }
608
-
609
- .modal {
610
- background-color: rgba(0, 0, 0, 0.5);
611
- }
612
-
613
- .modal-dialog {
614
- max-width: 600px;
615
- }
616
-
617
- .modal-header {
618
- background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
619
- border-bottom: 1px solid #e2e8f0;
620
- }
621
-
622
- .modal-title {
623
- color: #1e293b;
624
- font-weight: 600;
625
- }
626
-
627
- .modal-footer {
628
- background: #f8fafc;
629
- border-top: 1px solid #e2e8f0;
630
- }
631
-
632
- /* 执行结果区域 */
633
- .execution-results {
634
- border-top: 1px solid #e2e8f0;
635
- background: #f8fafc;
636
- }
637
-
638
- .results-header {
639
- display: flex;
640
- justify-content: space-between;
641
- align-items: center;
642
- padding: 1rem 1.5rem;
643
- background: linear-gradient(135deg, #f1f5f9 0%, #f8fafc 100%);
644
- border-bottom: 1px solid #e2e8f0;
645
- }
646
-
647
- .results-title {
648
- margin: 0;
649
- font-size: 1rem;
650
- font-weight: 600;
651
- color: #1e293b;
652
- display: flex;
653
- align-items: center;
654
- gap: 0.5rem;
655
- }
656
-
657
- .results-content {
658
- max-height: 400px;
659
- overflow-y: auto;
660
- padding: 1rem;
661
- }
662
-
663
- .no-results {
664
- display: flex;
665
- flex-direction: column;
666
- align-items: center;
667
- justify-content: center;
668
- padding: 3rem 1rem;
669
- color: #94a3b8;
670
- }
671
-
672
- .no-results i {
673
- font-size: 3rem;
674
- margin-bottom: 1rem;
675
- }
676
-
677
- .no-results p {
678
- margin: 0;
679
- font-size: 1rem;
680
- }
681
-
682
- .result-item {
683
- margin-bottom: 0.75rem;
684
- border: 1px solid #e2e8f0;
685
- border-radius: 8px;
686
- background: white;
687
- overflow: hidden;
688
- transition: box-shadow 0.2s;
689
- }
690
-
691
- .result-item:hover {
692
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
693
- }
694
-
695
- .result-item.result-success {
696
- border-left: 4px solid #22c55e;
697
- }
698
-
699
- .result-item.result-error {
700
- border-left: 4px solid #ef4444;
701
- }
702
-
703
- .result-item.result-info {
704
- border-left: 4px solid #3b82f6;
705
- }
706
-
707
- .result-header {
708
- display: flex;
709
- justify-content: space-between;
710
- align-items: center;
711
- padding: 0.75rem 1rem;
712
- cursor: pointer;
713
- background: white;
714
- transition: background 0.2s;
715
- }
716
-
717
- .result-header:hover {
718
- background: #f8fafc;
719
- }
720
-
721
- .result-title {
722
- display: flex;
723
- align-items: center;
724
- gap: 0.75rem;
725
- flex: 1;
726
- }
727
-
728
- .result-title i {
729
- font-size: 1.1rem;
730
- }
731
-
732
- .operation-name {
733
- font-weight: 600;
734
- color: #1e293b;
735
- font-size: 0.95rem;
736
- }
737
-
738
- .operation-time {
739
- color: #64748b;
740
- font-size: 0.85rem;
741
- margin-left: auto;
742
- }
743
-
744
- .toggle-icon {
745
- transition: transform 0.2s;
746
- color: #94a3b8;
747
- font-size: 0.9rem;
748
- }
749
-
750
- .toggle-icon.expanded {
751
- transform: rotate(180deg);
752
- }
753
-
754
- .result-body {
755
- padding: 1rem;
756
- background: #fafafa;
757
- border-top: 1px solid #e2e8f0;
758
- }
759
-
760
- .result-body pre {
761
- margin: 0;
762
- font-size: 0.85rem;
763
- line-height: 1.5;
764
- max-height: 300px;
765
- overflow: auto;
766
- }
767
-
768
- .result-body code {
769
- font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
770
- }
771
-
772
- /* JSON 语法高亮 - 不使用 scoped 以确保 v-html 内容能应用样式 */
773
- :deep(.json-key) {
774
- color: #d04255;
775
- font-weight: 500;
776
- }
777
-
778
- :deep(.json-string) {
779
- color: #22863a;
780
- }
781
-
782
- :deep(.json-number) {
783
- color: #005cc5;
784
- }
785
-
786
- :deep(.json-boolean) {
787
- color: #d73a49;
788
- }
789
-
790
- :deep(.json-null) {
791
- color: #6f42c1;
792
- }
793
-
794
- /* 滚动条样式 */
795
- .results-content::-webkit-scrollbar,
796
- .result-body pre::-webkit-scrollbar {
797
- width: 8px;
798
- height: 8px;
799
- }
800
-
801
- .results-content::-webkit-scrollbar-track,
802
- .result-body pre::-webkit-scrollbar-track {
803
- background: #f1f5f9;
804
- border-radius: 4px;
805
- }
806
-
807
- .results-content::-webkit-scrollbar-thumb,
808
- .result-body pre::-webkit-scrollbar-thumb {
809
- background: #cbd5e1;
810
- border-radius: 4px;
811
- }
812
-
813
- .results-content::-webkit-scrollbar-thumb:hover,
814
- .result-body pre::-webkit-scrollbar-thumb:hover {
815
- background: #94a3b8;
816
- }
1
+ <template>
2
+ <div class="db-tools">
3
+ <div class="tools-header">
4
+ <h5 class="tools-title">
5
+ <i class="bi bi-tools"></i>
6
+ 数据库管理工具
7
+ </h5>
8
+ </div>
9
+
10
+ <div class="tools-content">
11
+ <!-- 数据备份 -->
12
+ <div class="tool-section">
13
+ <h6 class="section-title">
14
+ <i class="bi bi-shield-check"></i>
15
+ 数据备份
16
+ </h6>
17
+ <div class="tool-actions">
18
+ <button class="btn btn-outline-primary btn-sm" @click="backupDatabase">
19
+ <i class="bi bi-download"></i> 备份数据库
20
+ </button>
21
+ <button class="btn btn-outline-secondary btn-sm" @click="showRestoreModal">
22
+ <i class="bi bi-upload"></i> 恢复数据库
23
+ </button>
24
+ <button class="btn btn-outline-info btn-sm" @click="showScheduleModal">
25
+ <i class="bi bi-clock"></i> 定时备份
26
+ </button>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- 用户管理 -->
31
+ <div class="tool-section">
32
+ <h6 class="section-title">
33
+ <i class="bi bi-people"></i>
34
+ 用户管理
35
+ </h6>
36
+ <div class="tool-actions">
37
+ <button class="btn btn-outline-success btn-sm" @click="showUsersList">
38
+ <i class="bi bi-person-lines-fill"></i> 用户列表
39
+ </button>
40
+ <button class="btn btn-outline-primary btn-sm" @click="showCreateUserModal">
41
+ <i class="bi bi-person-plus"></i> 创建用户
42
+ </button>
43
+ <button class="btn btn-outline-warning btn-sm" @click="showPermissionsModal">
44
+ <i class="bi bi-key"></i> 权限管理
45
+ </button>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- 性能监控 -->
50
+ <div class="tool-section">
51
+ <h6 class="section-title">
52
+ <i class="bi bi-speedometer2"></i>
53
+ 性能监控
54
+ </h6>
55
+ <div class="tool-actions">
56
+ <button class="btn btn-outline-info btn-sm" @click="showProcessList">
57
+ <i class="bi bi-activity"></i> 进程列表
58
+ </button>
59
+ <button class="btn btn-outline-warning btn-sm" @click="showSlowQueries">
60
+ <i class="bi bi-hourglass-split"></i> 慢查询
61
+ </button>
62
+ <button class="btn btn-outline-danger btn-sm" @click="showConnectionsList">
63
+ <i class="bi bi-diagram-3"></i> 连接数
64
+ </button>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- 数据库优化 -->
69
+ <div class="tool-section">
70
+ <h6 class="section-title">
71
+ <i class="bi bi-gear-wide-connected"></i>
72
+ 数据库优化
73
+ </h6>
74
+ <div class="tool-actions">
75
+ <button class="btn btn-outline-success btn-sm" @click="optimizeDatabase">
76
+ <i class="bi bi-lightning-charge"></i> 优化数据库
77
+ </button>
78
+ <button class="btn btn-outline-primary btn-sm" @click="analyzeTables">
79
+ <i class="bi bi-search"></i> 分析表
80
+ </button>
81
+ <button class="btn btn-outline-secondary btn-sm" @click="repairTables">
82
+ <i class="bi bi-tools"></i> 修复表
83
+ </button>
84
+ <button class="btn btn-outline-info btn-sm" @click="clearLogs">
85
+ <i class="bi bi-trash"></i> 清理日志
86
+ </button>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- 数据迁移 -->
91
+ <div class="tool-section">
92
+ <h6 class="section-title">
93
+ <i class="bi bi-arrow-left-right"></i>
94
+ 数据迁移
95
+ </h6>
96
+ <div class="tool-actions">
97
+ <button class="btn btn-outline-primary btn-sm" @click="showExportModal">
98
+ <i class="bi bi-box-arrow-up-right"></i> 导出结构
99
+ </button>
100
+ <button class="btn btn-outline-success btn-sm" @click="showImportModal">
101
+ <i class="bi bi-box-arrow-in-down"></i> 导入数据
102
+ </button>
103
+ <button class="btn btn-outline-warning btn-sm" @click="selectTool('sync')">
104
+ <i class="bi bi-arrow-repeat"></i> 数据同步
105
+ </button>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- 健康检查 -->
110
+ <div class="tool-section">
111
+ <h6 class="section-title">
112
+ <i class="bi bi-heart-pulse"></i>
113
+ 健康检查
114
+ </h6>
115
+ <div class="tool-actions">
116
+ <button class="btn btn-outline-info btn-sm" @click="runHealthCheck">
117
+ <i class="bi bi-clipboard-check"></i> 健康检查
118
+ </button>
119
+ <button class="btn btn-outline-secondary btn-sm" @click="showStatistics">
120
+ <i class="bi bi-bar-chart"></i> 数据统计
121
+ </button>
122
+ <button class="btn btn-outline-warning btn-sm" @click="showAuditLog">
123
+ <i class="bi bi-journal-text"></i> 审计日志
124
+ </button>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- 工具组件展示区域 -->
130
+ <div class="tool-component-area" v-if="selectedTool">
131
+ <div class="component-header">
132
+ <h6 class="component-title">
133
+ <i :class="getToolIcon(selectedTool)"></i>
134
+ {{ getToolTitle(selectedTool) }}
135
+ </h6>
136
+ <button class="btn btn-outline-secondary btn-sm" @click="closeTool">
137
+ <i class="bi bi-x"></i> 关闭
138
+ </button>
139
+ </div>
140
+
141
+ <!-- 数据同步组件 -->
142
+ <div v-if="selectedTool === 'sync'" class="tool-component sync-component">
143
+ <!-- 源数据库配置 -->
144
+ <div class="mb-4">
145
+ <h6 class="text-primary mb-2"><i class="bi bi-database"></i> 源数据库</h6>
146
+ <div class="row g-3">
147
+ <div class="col-md-6">
148
+ <label class="form-label">数据库名称</label>
149
+ <input type="text" class="form-control" v-model="syncConfig.source.database" readonly>
150
+ </div>
151
+ <div class="col-md-6">
152
+ <label class="form-label">选择表</label>
153
+ <select class="form-select" v-model="syncConfig.source.tableName">
154
+ <option value="">请选择表</option>
155
+ <option v-for="table in tables" :key="table.name" :value="table.name">{{ table.name }}</option>
156
+ </select>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- 目标数据库配置 -->
162
+ <div class="mb-4">
163
+ <h6 class="text-primary mb-2"><i class="bi bi-database"></i> 目标数据库</h6>
164
+
165
+ <!-- 连接模式选择 -->
166
+ <div class="mb-3">
167
+ <div class="form-check form-switch">
168
+ <input class="form-check-input" type="checkbox" v-model="useExistingConnection" id="useExistingConnection">
169
+ <label class="form-check-label" for="useExistingConnection">使用已配置的数据库连接</label>
170
+ </div>
171
+ </div>
172
+
173
+ <!-- 已配置连接选择 -->
174
+ <div v-if="useExistingConnection" class="row g-3">
175
+ <div class="col-md-6">
176
+ <label class="form-label">选择数据库连接</label>
177
+ <select class="form-select" v-model="selectedConnectionId">
178
+ <option value="">请选择连接</option>
179
+ <option v-for="conn in connections" :key="conn.id" :value="conn.id">{{ conn.name }} ({{ conn.type }})</option>
180
+ </select>
181
+ </div>
182
+ <div class="col-md-6">
183
+ <label class="form-label">选择数据库</label>
184
+ <select class="form-select" v-model="selectedDatabaseName">
185
+ <option value="">请选择数据库</option>
186
+ <option v-for="db in databases" :key="db" :value="db">{{ db }}</option>
187
+ </select>
188
+ </div>
189
+ <div class="col-md-12">
190
+ <label class="form-label">目标表名</label>
191
+ <input type="text" class="form-control" v-model="syncConfig.target.tableName">
192
+ </div>
193
+ </div>
194
+
195
+ <!-- 手动配置 -->
196
+ <div v-else class="row g-3">
197
+ <div class="col-md-4">
198
+ <label class="form-label">数据库类型</label>
199
+ <select class="form-select" v-model="syncConfig.target.dbType">
200
+ <option value="mysql">MySQL</option>
201
+ <option value="postgresql">PostgreSQL</option>
202
+ <option value="sqlite">SQLite</option>
203
+ <option value="sqlserver">SQL Server</option>
204
+ <option value="oracle">Oracle</option>
205
+ </select>
206
+ </div>
207
+ <div class="col-md-4">
208
+ <label class="form-label">主机</label>
209
+ <input type="text" class="form-control" v-model="syncConfig.target.host">
210
+ </div>
211
+ <div class="col-md-4">
212
+ <label class="form-label">端口</label>
213
+ <input type="number" class="form-control" v-model="syncConfig.target.port">
214
+ </div>
215
+ <div class="col-md-4">
216
+ <label class="form-label">数据库名</label>
217
+ <input type="text" class="form-control" v-model="syncConfig.target.database">
218
+ </div>
219
+ <div class="col-md-4">
220
+ <label class="form-label">用户名</label>
221
+ <input type="text" class="form-control" v-model="syncConfig.target.username">
222
+ </div>
223
+ <div class="col-md-4">
224
+ <label class="form-label">密码</label>
225
+ <input type="password" class="form-control" v-model="syncConfig.target.password">
226
+ </div>
227
+ <div class="col-md-6">
228
+ <label class="form-label">目标表名</label>
229
+ <input type="text" class="form-control" v-model="syncConfig.target.tableName">
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <!-- 同步选项 -->
235
+ <div class="mb-4">
236
+ <h6 class="text-primary mb-2"><i class="bi bi-sliders"></i> 同步选项</h6>
237
+ <div class="row g-3">
238
+ <div class="col-md-6">
239
+ <div class="form-check">
240
+ <input type="checkbox" class="form-check-input" v-model="syncConfig.options.syncStructure" id="syncStructure">
241
+ <label class="form-check-label" for="syncStructure">同步表结构</label>
242
+ </div>
243
+ </div>
244
+ <div class="col-md-6">
245
+ <div class="form-check">
246
+ <input type="checkbox" class="form-check-input" v-model="syncConfig.options.syncData" id="syncData">
247
+ <label class="form-check-label" for="syncData">同步表数据</label>
248
+ </div>
249
+ </div>
250
+ <div class="col-md-6">
251
+ <div class="form-check">
252
+ <input type="checkbox" class="form-check-input" v-model="syncConfig.options.dropIfExists" id="dropIfExists">
253
+ <label class="form-check-label" for="dropIfExists">目标表存在时删除</label>
254
+ </div>
255
+ </div>
256
+ <div class="col-md-6">
257
+ <div class="form-check">
258
+ <input type="checkbox" class="form-check-input" v-model="syncConfig.options.bulkInsert" id="bulkInsert">
259
+ <label class="form-check-label" for="bulkInsert">批量插入数据</label>
260
+ </div>
261
+ </div>
262
+ <div class="col-md-6">
263
+ <div class="form-check">
264
+ <input type="checkbox" class="form-check-input" v-model="syncConfig.options.overrideExisting" id="overrideExisting">
265
+ <label class="form-check-label" for="overrideExisting">覆盖已存在的数据</label>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+
271
+ <!-- 操作按钮 -->
272
+ <div class="tool-actions">
273
+ <button class="btn btn-primary btn-sm" @click="performSync" :disabled="syncing || !isSyncFormValid">
274
+ <i class="bi bi-play-fill"></i> 开始同步
275
+ </button>
276
+ <button v-if="syncing" class="btn btn-outline-danger btn-sm" @click="stopSync">
277
+ <i class="bi bi-stop-fill"></i> 停止同步
278
+ </button>
279
+ </div>
280
+ </div>
281
+ </div>
282
+
283
+ <!-- 执行结果展示区域 -->
284
+ <div class="execution-results">
285
+ <div class="results-header">
286
+ <h6 class="results-title">
287
+ <i class="bi bi-terminal"></i>
288
+ 执行结果
289
+ </h6>
290
+ <button class="btn btn-outline-secondary btn-sm" @click="clearResults">
291
+ <i class="bi bi-trash"></i> 清空
292
+ </button>
293
+ </div>
294
+ <div class="results-content" ref="resultsContentRef">
295
+ <div v-if="executionResults.length === 0" class="no-results">
296
+ <i class="bi bi-inbox"></i>
297
+ <p>暂无执行结果</p>
298
+ </div>
299
+ <div v-for="(result, index) in executionResults" :key="index" class="result-item" :class="`result-${result.status}`">
300
+ <div class="result-header" @click="toggleResult(index)">
301
+ <div class="result-title">
302
+ <i :class="getResultIcon(result.status)"></i>
303
+ <span class="operation-name">{{ result.operation }}</span>
304
+ <span class="operation-time">{{ result.timestamp }}</span>
305
+ </div>
306
+ <i class="bi bi-chevron-down toggle-icon" :class="{ 'expanded': result.expanded }"></i>
307
+ </div>
308
+ <div v-if="result.expanded" class="result-body">
309
+ <pre><code v-html="highlightJson(result.data)"></code></pre>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ <!-- 数据恢复模态框 -->
316
+ <div class="modal fade" :class="{ show: restoreModalVisible }" :style="{ display: restoreModalVisible ? 'block' : 'none', zIndex: 1055 }">
317
+ <div class="modal-dialog">
318
+ <div class="modal-content">
319
+ <div class="modal-header">
320
+ <h5 class="modal-title">恢复数据库</h5>
321
+ <button type="button" class="btn-close" @click="closeRestoreModal"></button>
322
+ </div>
323
+ <div class="modal-body">
324
+ <p>请选择要恢复的备份文件:</p>
325
+ <div class="mb-3">
326
+ <input type="file" class="form-control" @change="handleFileChange" accept=".sql,.bak">
327
+ </div>
328
+ <div v-if="selectedFile" class="alert alert-info">
329
+ 已选择文件:{{ selectedFile.name }}
330
+ </div>
331
+ <div class="mb-3 form-check">
332
+ <input type="checkbox" class="form-check-input" v-model="restoreOptions.dropExisting" id="dropExisting">
333
+ <label class="form-check-label" for="dropExisting">删除现有表</label>
334
+ </div>
335
+ </div>
336
+ <div class="modal-footer">
337
+ <button type="button" class="btn btn-secondary" @click="closeRestoreModal">取消</button>
338
+ <button type="button" class="btn btn-primary" @click="performRestore" :disabled="!selectedFile">
339
+ <span v-if="restoring" class="spinner-border spinner-border-sm me-2"></span>
340
+ 恢复
341
+ </button>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+
348
+ </div>
349
+ </template>
350
+
351
+ <script lang="ts" setup>
352
+ import { ref, computed, watch, onMounted } from 'vue';
353
+ import { DatabaseService, ConnectionService } from '@/service/database';
354
+ import { modal } from '@/utils/modal';
355
+ import { toast } from '@/utils/toast';
356
+
357
+ const connectionService = new ConnectionService();
358
+
359
+ const props = defineProps<{
360
+ connection: any;
361
+ database: string;
362
+ }>();
363
+
364
+ const emit = defineEmits<{
365
+ 'execute-sql': [sql: string];
366
+ }>();
367
+
368
+ const databaseService = new DatabaseService();
369
+
370
+ // 状态管理
371
+ const restoreModalVisible = ref(false);
372
+ const selectedFile = ref<File | null>(null);
373
+ const restoring = ref(false);
374
+ const resultsContentRef = ref<HTMLElement | null>(null);
375
+
376
+ // 工具组件状态
377
+ const selectedTool = ref<string | null>(null);
378
+
379
+ // 同步功能状态
380
+ const syncing = ref(false);
381
+ const tables = ref<any[]>([]);
382
+ const connections = ref<any[]>([]);
383
+ const databases = ref<any[]>([]);
384
+ const useExistingConnection = ref(false);
385
+ const selectedConnectionId = ref('');
386
+ const selectedDatabaseName = ref('');
387
+
388
+ // 同步配置
389
+ const syncConfig = ref({
390
+ source: {
391
+ database: '',
392
+ tableName: ''
393
+ },
394
+ target: {
395
+ dbType: 'mysql',
396
+ host: 'localhost',
397
+ port: 3306,
398
+ database: '',
399
+ username: 'root',
400
+ password: '',
401
+ tableName: ''
402
+ },
403
+ options: {
404
+ syncStructure: true,
405
+ syncData: true,
406
+ dropIfExists: false,
407
+ bulkInsert: true,
408
+ overrideExisting: false
409
+ }
410
+ });
411
+
412
+ // 监听源表名变化,自动更新目标表名
413
+ watch(() => syncConfig.value.source.tableName, (newTableName) => {
414
+ if (newTableName) {
415
+ syncConfig.value.target.tableName = newTableName;
416
+ }
417
+ });
418
+
419
+ // 组件挂载时初始化同步数据
420
+ onMounted(() => {
421
+ initSyncData();
422
+ });
423
+
424
+ // 监听连接ID变化,加载数据库列表
425
+ async function loadDatabases(connectionId: string) {
426
+ if (!connectionId) {
427
+ databases.value = [];
428
+ selectedDatabaseName.value = '';
429
+ return;
430
+ }
431
+
432
+ try {
433
+ const res = await databaseService.getDatabases(connectionId);
434
+ if (res.ret === 0) {
435
+ databases.value = res.data || [];
436
+ } else {
437
+ databases.value = [];
438
+ }
439
+ selectedDatabaseName.value = '';
440
+ } catch (error) {
441
+ console.error('加载数据库列表失败:', error);
442
+ databases.value = [];
443
+ selectedDatabaseName.value = '';
444
+ }
445
+ }
446
+
447
+ // 监听连接ID变化
448
+ watch(selectedConnectionId, (newVal) => {
449
+ loadDatabases(newVal);
450
+ });
451
+
452
+ // 执行结果历史
453
+ interface ExecutionResult {
454
+ operation: string;
455
+ status: 'success' | 'error' | 'info';
456
+ timestamp: string;
457
+ data: any;
458
+ expanded: boolean;
459
+ }
460
+
461
+ const executionResults = ref<ExecutionResult[]>([]);
462
+
463
+ const restoreOptions = ref({
464
+ dropExisting: false
465
+ });
466
+
467
+ // 验证同步表单
468
+ const isSyncFormValid = computed(() => {
469
+ if (useExistingConnection.value) {
470
+ return syncConfig.value.source.tableName &&
471
+ selectedConnectionId.value &&
472
+ selectedDatabaseName.value &&
473
+ syncConfig.value.target.tableName &&
474
+ (syncConfig.value.options.syncStructure || syncConfig.value.options.syncData);
475
+ } else {
476
+ return syncConfig.value.source.tableName &&
477
+ syncConfig.value.target.host &&
478
+ syncConfig.value.target.port &&
479
+ syncConfig.value.target.database &&
480
+ syncConfig.value.target.username &&
481
+ syncConfig.value.target.tableName &&
482
+ (syncConfig.value.options.syncStructure || syncConfig.value.options.syncData);
483
+ }
484
+ });
485
+
486
+ // 添加执行结果
487
+ function addExecutionResult(operation: string, status: 'success' | 'error' | 'info', data: any) {
488
+ const timestamp = new Date().toLocaleString('zh-CN', {
489
+ year: 'numeric',
490
+ month: '2-digit',
491
+ day: '2-digit',
492
+ hour: '2-digit',
493
+ minute: '2-digit',
494
+ second: '2-digit'
495
+ });
496
+
497
+ executionResults.value.unshift({
498
+ operation,
499
+ status,
500
+ timestamp,
501
+ data,
502
+ expanded: false
503
+ });
504
+
505
+ // 只保留最近50条记录
506
+ if (executionResults.value.length > 50) {
507
+ executionResults.value = executionResults.value.slice(0, 50);
508
+ }
509
+
510
+ // 自动滚动到底部(显示最新结果在顶部,所以滚动到0)
511
+ setTimeout(() => {
512
+ if (resultsContentRef.value) {
513
+ resultsContentRef.value.scrollTop = 0;
514
+ }
515
+ }, 100);
516
+ }
517
+
518
+ // 清空执行结果
519
+ function clearResults() {
520
+ executionResults.value = [];
521
+ }
522
+
523
+ // 切换结果展开/收起
524
+ function toggleResult(index: number) {
525
+ const result = executionResults.value[index];
526
+ if (result) {
527
+ result.expanded = !result.expanded;
528
+ }
529
+ }
530
+
531
+ // 格式化错误信息
532
+ function formatError(error: any): any {
533
+ const formatted: any = {
534
+ success: false,
535
+ message: error.msg || error.message || '未知错误'
536
+ };
537
+ if (error.stack) {
538
+ formatted.stack = error.stack;
539
+ }
540
+ return formatted;
541
+ }
542
+
543
+ // JSON 语法高亮
544
+ function highlightJson(data: any): string {
545
+ if (data === null || data === undefined) return '';
546
+ const jsonStr = JSON.stringify(data, null, 2);
547
+ return jsonStr.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
548
+ let cls = 'json-number';
549
+ if (/^"/.test(match)) {
550
+ if (/:$/.test(match)) {
551
+ cls = 'json-key';
552
+ } else {
553
+ cls = 'json-string';
554
+ }
555
+ } else if (/true|false/.test(match)) {
556
+ cls = 'json-boolean';
557
+ } else if (/null/.test(match)) {
558
+ cls = 'json-null';
559
+ }
560
+ return '<span class="' + cls + '">' + match + '</span>';
561
+ });
562
+ }
563
+
564
+ // 获取结果图标
565
+ function getResultIcon(status: string): string {
566
+ switch (status) {
567
+ case 'success':
568
+ return 'bi bi-check-circle-fill text-success';
569
+ case 'error':
570
+ return 'bi bi-x-circle-fill text-danger';
571
+ case 'info':
572
+ return 'bi bi-info-circle-fill text-info';
573
+ default:
574
+ return 'bi bi-dash-circle-fill text-secondary';
575
+ }
576
+ }
577
+
578
+ // 数据备份
579
+ async function backupDatabase() {
580
+ const operation = '备份数据库';
581
+ try {
582
+ const res = await databaseService.backupDatabase(props.connection?.id || '', props.database);
583
+ if(res.ret === 0) {
584
+ addExecutionResult(operation, 'success', res);
585
+ } else {
586
+ modal.error(res.msg || '备份失败');
587
+ addExecutionResult(operation, 'error', formatError(res));
588
+ }
589
+ } catch (error: any) {
590
+ console.error('备份失败:', error);
591
+ modal.error(error.msg || error.message || '备份失败');
592
+ addExecutionResult(operation, 'error', formatError(error));
593
+ }
594
+ }
595
+
596
+ // 用户管理
597
+ function showUsersList() {
598
+ addExecutionResult('用户列表', 'info', { message: '用户列表功能开发中...' });
599
+ }
600
+
601
+ function showCreateUserModal() {
602
+ addExecutionResult('创建用户', 'info', { message: '创建用户功能开发中...' });
603
+ }
604
+
605
+ function showPermissionsModal() {
606
+ addExecutionResult('权限管理', 'info', { message: '权限管理功能开发中...' });
607
+ }
608
+
609
+ // 性能监控
610
+ function showProcessList() {
611
+ const sql = 'SHOW PROCESSLIST';
612
+ addExecutionResult('进程列表', 'info', { sql: sql, message: '已发送 SQL 查询' });
613
+ emit('execute-sql', sql);
614
+ }
615
+
616
+ function showSlowQueries() {
617
+ const sql = 'SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10';
618
+ addExecutionResult('慢查询', 'info', { sql: sql, message: '已发送 SQL 查询' });
619
+ emit('execute-sql', sql);
620
+ }
621
+
622
+ function showConnectionsList() {
623
+ const sql = 'SHOW STATUS LIKE "Threads_connected"';
624
+ addExecutionResult('连接数', 'info', { sql: sql, message: '已发送 SQL 查询' });
625
+ emit('execute-sql', sql);
626
+ }
627
+
628
+ // 数据库优化
629
+ async function optimizeDatabase() {
630
+ const operation = '优化数据库';
631
+ try {
632
+ const res = await databaseService.optimizeDatabase(props.connection?.id || '', props.database);
633
+ if(res.ret === 0) {
634
+ addExecutionResult(operation, 'success', res.data);
635
+ } else {
636
+ modal.error(res.msg || '优化失败');
637
+ addExecutionResult(operation, 'error', formatError(res));
638
+ }
639
+ } catch (error: any) {
640
+ console.error('优化失败:', error);
641
+ modal.error(error.msg || error.message || '优化失败');
642
+ addExecutionResult(operation, 'error', formatError(error));
643
+ }
644
+ }
645
+
646
+ async function analyzeTables() {
647
+ const operation = '分析表';
648
+ try {
649
+ const res = await databaseService.analyzeTables(props.connection?.id || '', props.database);
650
+ if(res.ret === 0) {
651
+ addExecutionResult(operation, 'success', res.data);
652
+ } else {
653
+ modal.error(res.msg || '分析失败');
654
+ addExecutionResult(operation, 'error', formatError(res));
655
+ }
656
+ } catch (error: any) {
657
+ console.error('分析失败:', error);
658
+ modal.error(res.msg || error.message || '分析失败');
659
+ addExecutionResult(operation, 'error', formatError(error));
660
+ }
661
+ }
662
+
663
+ async function repairTables() {
664
+ const operation = '修复表';
665
+ try {
666
+ const res = await databaseService.repairTables(props.connection?.id || '', props.database);
667
+ if(res.ret === 0) {
668
+ addExecutionResult(operation, 'success', res.data);
669
+ } else {
670
+ modal.error(res.msg || '修复失败');
671
+ addExecutionResult(operation, 'error', formatError(res));
672
+ }
673
+ } catch (error: any) {
674
+ console.error('修复失败:', error);
675
+ modal.error(res.msg || error.message || '修复失败');
676
+ addExecutionResult(operation, 'error', formatError(error));
677
+ }
678
+ }
679
+
680
+ async function clearLogs() {
681
+ const operation = '清理日志';
682
+ const logs = [
683
+ 'TRUNCATE TABLE mysql.slow_log',
684
+ 'TRUNCATE TABLE mysql.general_log',
685
+ 'FLUSH LOGS'
686
+ ];
687
+
688
+ logs.forEach(sql => {
689
+ addExecutionResult(`清理日志 - ${sql.split(' ')[1]}`, 'info', { sql, message: '已发送 SQL 查询' });
690
+ emit('execute-sql', sql);
691
+ });
692
+ }
693
+
694
+ // 数据迁移
695
+ function showExportModal() {
696
+ addExecutionResult('导出结构', 'info', { message: '导出结构功能开发中...' });
697
+ }
698
+
699
+ function showImportModal() {
700
+ addExecutionResult('导入数据', 'info', { message: '导入数据功能开发中...' });
701
+ }
702
+
703
+ // 健康检查
704
+ async function runHealthCheck() {
705
+ const operation = '健康检查';
706
+ const checks = [
707
+ { name: '连接状态', sql: 'SELECT 1 as status' },
708
+ { name: '表完整性', sql: 'SELECT COUNT(*) as status FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = "BASE TABLE"' },
709
+ { name: '索引状态', sql: 'SELECT COUNT(*) as status FROM information_schema.statistics WHERE table_schema = DATABASE()' },
710
+ { name: '磁盘空间', sql: 'SELECT SUM(data_length + index_length) as status FROM information_schema.tables WHERE table_schema = DATABASE()' }
711
+ ];
712
+
713
+ const results: any[] = [];
714
+ for (const check of checks) {
715
+ try {
716
+ // 这里应该调用实际的数据库查询
717
+ results.push({
718
+ name: check.name,
719
+ status: 'healthy',
720
+ message: '正常'
721
+ });
722
+ } catch (error: any) {
723
+ results.push({
724
+ name: check.name,
725
+ status: 'error',
726
+ message: error.message
727
+ });
728
+ }
729
+ }
730
+
731
+ addExecutionResult(operation, 'success', { checks: results });
732
+ }
733
+
734
+ function showStatistics() {
735
+ const sql = `
736
+ SELECT
737
+ table_name as '表名',
738
+ table_rows as '记录数',
739
+ ROUND(((data_length + index_length) / 1024 / 1024), 2) as '大小(MB)'
740
+ FROM information_schema.tables
741
+ WHERE table_schema = DATABASE()
742
+ ORDER BY (data_length + index_length) DESC
743
+ `;
744
+ addExecutionResult('数据统计', 'info', { sql: sql, message: '已发送 SQL 查询' });
745
+ emit('execute-sql', sql);
746
+ }
747
+
748
+ function showAuditLog() {
749
+ const sql = 'SELECT * FROM mysql.general_log ORDER BY event_time DESC LIMIT 100';
750
+ addExecutionResult('审计日志', 'info', { sql: sql, message: '已发送 SQL 查询' });
751
+ emit('execute-sql', sql);
752
+ }
753
+
754
+ // 恢复功能
755
+ function showRestoreModal() {
756
+ restoreModalVisible.value = true;
757
+ }
758
+
759
+ function closeRestoreModal() {
760
+ restoreModalVisible.value = false;
761
+ selectedFile.value = null;
762
+ }
763
+
764
+ function handleFileSelect(event: Event) {
765
+ const target = event.target as HTMLInputElement;
766
+ if (target.files && target.files.length > 0) {
767
+ selectedFile.value = target.files[0] as File;
768
+ }
769
+ }
770
+
771
+ async function performRestore() {
772
+ if (!selectedFile.value) return;
773
+
774
+ const operation = '恢复数据库';
775
+ try {
776
+ restoring.value = true;
777
+ const filePath = selectedFile.value.name;
778
+
779
+ const res = await databaseService.restoreDatabase(
780
+ props.connection?.id || '',
781
+ props.database,
782
+ filePath,
783
+ { dropExisting: restoreOptions.value.dropExisting }
784
+ );
785
+
786
+ addExecutionResult(operation, 'success', res);
787
+ closeRestoreModal();
788
+ } catch (error: any) {
789
+ console.error('恢复失败:', error);
790
+ modal.error(error.msg || error.message || '恢复失败');
791
+ addExecutionResult(operation, 'error', formatError(error));
792
+ } finally {
793
+ restoring.value = false;
794
+ }
795
+ }
796
+
797
+ // 选择工具
798
+ function selectTool(toolName: string) {
799
+ selectedTool.value = toolName;
800
+ if (toolName === 'sync') {
801
+ initSyncData();
802
+ }
803
+ }
804
+
805
+ // 关闭工具
806
+ function closeTool() {
807
+ selectedTool.value = null;
808
+ }
809
+
810
+ // 获取工具图标
811
+ function getToolIcon(toolName: string) {
812
+ const icons: Record<string, string> = {
813
+ 'sync': 'bi-arrow-repeat'
814
+ };
815
+ return icons[toolName] || 'bi-gear';
816
+ }
817
+
818
+ // 获取工具标题
819
+ function getToolTitle(toolName: string) {
820
+ const titles: Record<string, string> = {
821
+ 'sync': '数据同步'
822
+ };
823
+ return titles[toolName] || '工具';
824
+ }
825
+
826
+ // 同步功能 - 初始化数据
827
+ async function initSyncData() {
828
+ try {
829
+ // 加载表列表
830
+ const tablesRes = await databaseService.getTables(props.connection?.id || '', props.database);
831
+ if (tablesRes.ret === 0) {
832
+ tables.value = tablesRes.data || [];
833
+ }
834
+
835
+ // 加载已配置的数据库连接列表
836
+ const connRes = await connectionService.getAllConnections();
837
+ if (connRes.ret === 0) {
838
+ connections.value = connRes.data || [];
839
+ }
840
+
841
+ // 设置源数据库信息
842
+ syncConfig.value.source.database = props.database;
843
+
844
+ // 默认选择当前连接
845
+ if (props.connection?.id) {
846
+ useExistingConnection.value = true;
847
+ selectedConnectionId.value = props.connection.id;
848
+ }
849
+ } catch (error: any) {
850
+ console.error('加载表列表失败:', error);
851
+ modal.error('加载表列表失败');
852
+ }
853
+ }
854
+
855
+ // 重置同步状态
856
+ function resetSyncState() {
857
+ syncing.value = false;
858
+ tables.value = [];
859
+ databases.value = [];
860
+ useExistingConnection.value = false;
861
+ selectedConnectionId.value = '';
862
+ selectedDatabaseName.value = '';
863
+ syncConfig.value = {
864
+ source: {
865
+ database: '',
866
+ tableName: ''
867
+ },
868
+ target: {
869
+ dbType: 'mysql',
870
+ host: 'localhost',
871
+ port: 3306,
872
+ database: '',
873
+ username: 'root',
874
+ password: '',
875
+ tableName: ''
876
+ },
877
+ options: {
878
+ syncStructure: true,
879
+ syncData: true,
880
+ dropIfExists: false,
881
+ bulkInsert: true,
882
+ overrideExisting: false
883
+ }
884
+ };
885
+ }
886
+
887
+ async function performSync() {
888
+ if (!isSyncFormValid.value) {
889
+ modal.error('请填写完整的同步配置');
890
+ return;
891
+ }
892
+
893
+ const operation = '数据同步';
894
+ syncing.value = true;
895
+
896
+ try {
897
+ // 构建同步配置
898
+ let syncData;
899
+ if (useExistingConnection.value) {
900
+ // 使用已配置连接
901
+ syncData = {
902
+ source: {
903
+ database: syncConfig.value.source.database,
904
+ tableName: syncConfig.value.source.tableName
905
+ },
906
+ target: {
907
+ connectionId: selectedConnectionId.value,
908
+ database: selectedDatabaseName.value,
909
+ tableName: syncConfig.value.target.tableName
910
+ },
911
+ options: syncConfig.value.options
912
+ };
913
+ } else {
914
+ // 使用手动配置
915
+ syncData = syncConfig.value;
916
+ }
917
+
918
+ // 添加同步开始记录
919
+ addExecutionResult(operation, 'info', {
920
+ message: '开始同步数据',
921
+ config: syncData
922
+ });
923
+
924
+ // 执行同步
925
+ const res = await databaseService.syncTable(
926
+ props.connection?.id || '',
927
+ syncData
928
+ );
929
+
930
+ if (res.ret === 0) {
931
+ const tables = res.data?.tables || [];
932
+ let successCount = 0;
933
+ let totalRows = 0;
934
+
935
+ tables.forEach((table: any) => {
936
+ if (table.rowsSynced > 0) {
937
+ successCount++;
938
+ totalRows += table.rowsSynced;
939
+ }
940
+ });
941
+
942
+ addExecutionResult(operation, 'success', {
943
+ message: `数据同步成功,${successCount}/${tables.length} 个表同步完成,共同步 ${totalRows} 行数据`,
944
+ data: res.data
945
+ });
946
+ toast.success(`数据同步成功,${successCount}/${tables.length} 个表同步完成`);
947
+ } else {
948
+ addExecutionResult(operation, 'error', {
949
+ message: res.msg || '同步失败',
950
+ error: res.error
951
+ });
952
+ toast.error(res.msg || '同步失败');
953
+ }
954
+ } catch (error: any) {
955
+ console.error('同步失败:', error);
956
+ addExecutionResult(operation, 'error', formatError(error));
957
+ toast.error(error.msg || error.message || '同步失败');
958
+ } finally {
959
+ syncing.value = false;
960
+ }
961
+ }
962
+
963
+ function showScheduleModal() {
964
+ addExecutionResult('定时备份', 'info', { message: '定时备份功能开发中...' });
965
+ }
966
+ </script>
967
+
968
+ <style scoped>
969
+ .db-tools {
970
+ background: white;
971
+ border-radius: 12px;
972
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
973
+ }
974
+
975
+ .tools-header {
976
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
977
+ color: white;
978
+ padding: 1rem 1.5rem;
979
+ border-bottom: 1px solid #e2e8f0;
980
+ }
981
+
982
+ .tools-title {
983
+ margin: 0;
984
+ font-size: 1.1rem;
985
+ font-weight: 600;
986
+ display: flex;
987
+ align-items: center;
988
+ gap: 0.5rem;
989
+ }
990
+
991
+ .tools-content {
992
+ padding: 1.5rem;
993
+ overflow-y: auto;
994
+ }
995
+
996
+ .tool-section {
997
+ margin-bottom: 2rem;
998
+ }
999
+
1000
+ .section-title {
1001
+ font-size: 0.9rem;
1002
+ font-weight: 600;
1003
+ color: #374151;
1004
+ margin-bottom: 1rem;
1005
+ display: flex;
1006
+ align-items: center;
1007
+ gap: 0.5rem;
1008
+ }
1009
+
1010
+ .tool-actions {
1011
+ display: flex;
1012
+ flex-wrap: wrap;
1013
+ gap: 0.5rem;
1014
+ }
1015
+
1016
+ .tool-actions .btn {
1017
+ min-width: 120px;
1018
+ display: flex;
1019
+ align-items: center;
1020
+ gap: 0.5rem;
1021
+ }
1022
+
1023
+ .modal {
1024
+ background-color: rgba(0, 0, 0, 0.5);
1025
+ }
1026
+
1027
+ .modal-dialog {
1028
+ max-width: 600px;
1029
+ }
1030
+
1031
+ .modal-header {
1032
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
1033
+ border-bottom: 1px solid #e2e8f0;
1034
+ }
1035
+
1036
+ .modal-title {
1037
+ color: #1e293b;
1038
+ font-weight: 600;
1039
+ }
1040
+
1041
+ .modal-footer {
1042
+ background: #f8fafc;
1043
+ border-top: 1px solid #e2e8f0;
1044
+ }
1045
+
1046
+ /* 工具组件区域 */
1047
+ .tool-component-area {
1048
+ border-top: 1px solid #e2e8f0;
1049
+ background: #f8fafc;
1050
+ }
1051
+
1052
+ .component-header {
1053
+ display: flex;
1054
+ justify-content: space-between;
1055
+ align-items: center;
1056
+ padding: 1rem 1.5rem;
1057
+ background: linear-gradient(135deg, #f1f5f9 0%, #f8fafc 100%);
1058
+ border-bottom: 1px solid #e2e8f0;
1059
+ }
1060
+
1061
+ .component-title {
1062
+ margin: 0;
1063
+ font-size: 1rem;
1064
+ font-weight: 600;
1065
+ color: #1e293b;
1066
+ display: flex;
1067
+ align-items: center;
1068
+ gap: 0.5rem;
1069
+ }
1070
+
1071
+ .tool-component {
1072
+ padding: 1.5rem;
1073
+ }
1074
+
1075
+ .sync-component {
1076
+ background: white;
1077
+ border-radius: 0.375rem;
1078
+ }
1079
+
1080
+ /* 执行结果区域 */
1081
+ .execution-results {
1082
+ border-top: 1px solid #e2e8f0;
1083
+ background: #f8fafc;
1084
+ }
1085
+
1086
+ .results-header {
1087
+ display: flex;
1088
+ justify-content: space-between;
1089
+ align-items: center;
1090
+ padding: 1rem 1.5rem;
1091
+ background: linear-gradient(135deg, #f1f5f9 0%, #f8fafc 100%);
1092
+ border-bottom: 1px solid #e2e8f0;
1093
+ }
1094
+
1095
+ .results-title {
1096
+ margin: 0;
1097
+ font-size: 1rem;
1098
+ font-weight: 600;
1099
+ color: #1e293b;
1100
+ display: flex;
1101
+ align-items: center;
1102
+ gap: 0.5rem;
1103
+ }
1104
+
1105
+ .results-content {
1106
+ max-height: 400px;
1107
+ overflow-y: auto;
1108
+ padding: 1rem;
1109
+ }
1110
+
1111
+ .no-results {
1112
+ display: flex;
1113
+ flex-direction: column;
1114
+ align-items: center;
1115
+ justify-content: center;
1116
+ padding: 3rem 1rem;
1117
+ color: #94a3b8;
1118
+ }
1119
+
1120
+ .no-results i {
1121
+ font-size: 3rem;
1122
+ margin-bottom: 1rem;
1123
+ }
1124
+
1125
+ .no-results p {
1126
+ margin: 0;
1127
+ font-size: 1rem;
1128
+ }
1129
+
1130
+ .result-item {
1131
+ margin-bottom: 0.75rem;
1132
+ border: 1px solid #e2e8f0;
1133
+ border-radius: 8px;
1134
+ background: white;
1135
+ overflow: hidden;
1136
+ transition: box-shadow 0.2s;
1137
+ }
1138
+
1139
+ .result-item:hover {
1140
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1141
+ }
1142
+
1143
+ .result-item.result-success {
1144
+ border-left: 4px solid #22c55e;
1145
+ }
1146
+
1147
+ .result-item.result-error {
1148
+ border-left: 4px solid #ef4444;
1149
+ }
1150
+
1151
+ .result-item.result-info {
1152
+ border-left: 4px solid #3b82f6;
1153
+ }
1154
+
1155
+ .result-header {
1156
+ display: flex;
1157
+ justify-content: space-between;
1158
+ align-items: center;
1159
+ padding: 0.75rem 1rem;
1160
+ cursor: pointer;
1161
+ background: white;
1162
+ transition: background 0.2s;
1163
+ }
1164
+
1165
+ .result-header:hover {
1166
+ background: #f8fafc;
1167
+ }
1168
+
1169
+ .result-title {
1170
+ display: flex;
1171
+ align-items: center;
1172
+ gap: 0.75rem;
1173
+ flex: 1;
1174
+ }
1175
+
1176
+ .result-title i {
1177
+ font-size: 1.1rem;
1178
+ }
1179
+
1180
+ .operation-name {
1181
+ font-weight: 600;
1182
+ color: #1e293b;
1183
+ font-size: 0.95rem;
1184
+ }
1185
+
1186
+ .operation-time {
1187
+ color: #64748b;
1188
+ font-size: 0.85rem;
1189
+ margin-left: auto;
1190
+ }
1191
+
1192
+ .toggle-icon {
1193
+ transition: transform 0.2s;
1194
+ color: #94a3b8;
1195
+ font-size: 0.9rem;
1196
+ }
1197
+
1198
+ .toggle-icon.expanded {
1199
+ transform: rotate(180deg);
1200
+ }
1201
+
1202
+ .result-body {
1203
+ padding: 1rem;
1204
+ background: #fafafa;
1205
+ border-top: 1px solid #e2e8f0;
1206
+ }
1207
+
1208
+ .result-body pre {
1209
+ margin: 0;
1210
+ font-size: 0.85rem;
1211
+ line-height: 1.5;
1212
+ max-height: 300px;
1213
+ overflow: auto;
1214
+ }
1215
+
1216
+ .result-body code {
1217
+ font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
1218
+ }
1219
+
1220
+ /* JSON 语法高亮 - 不使用 scoped 以确保 v-html 内容能应用样式 */
1221
+ :deep(.json-key) {
1222
+ color: #d04255;
1223
+ font-weight: 500;
1224
+ }
1225
+
1226
+ :deep(.json-string) {
1227
+ color: #22863a;
1228
+ }
1229
+
1230
+ :deep(.json-number) {
1231
+ color: #005cc5;
1232
+ }
1233
+
1234
+ :deep(.json-boolean) {
1235
+ color: #d73a49;
1236
+ }
1237
+
1238
+ :deep(.json-null) {
1239
+ color: #6f42c1;
1240
+ }
1241
+
1242
+ /* 滚动条样式 */
1243
+ .results-content::-webkit-scrollbar,
1244
+ .result-body pre::-webkit-scrollbar {
1245
+ width: 8px;
1246
+ height: 8px;
1247
+ }
1248
+
1249
+ .results-content::-webkit-scrollbar-track,
1250
+ .result-body pre::-webkit-scrollbar-track {
1251
+ background: #f1f5f9;
1252
+ border-radius: 4px;
1253
+ }
1254
+
1255
+ .results-content::-webkit-scrollbar-thumb,
1256
+ .result-body pre::-webkit-scrollbar-thumb {
1257
+ background: #cbd5e1;
1258
+ border-radius: 4px;
1259
+ }
1260
+
1261
+ .results-content::-webkit-scrollbar-thumb:hover,
1262
+ .result-body pre::-webkit-scrollbar-thumb:hover {
1263
+ background: #94a3b8;
1264
+ }
817
1265
  </style>