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,1349 +1,1349 @@
1
- <template>
2
- <div class="query-history">
3
- <div class="history-header">
4
- <div class="header-title">
5
- <i class="bi bi-clock-history"></i>
6
- <span>查询历史</span>
7
- </div>
8
- <div class="header-controls">
9
- <div class="search-box">
10
- <i class="bi bi-search"></i>
11
- <input
12
- type="text"
13
- v-model="searchQuery"
14
- placeholder="搜索查询..."
15
- @input="handleSearch"
16
- >
17
- </div>
18
- <select v-model="filterType" class="filter-select" @change="filterQueries">
19
- <option value="all">所有类型</option>
20
- <option value="SELECT">SELECT</option>
21
- <option value="INSERT">INSERT</option>
22
- <option value="UPDATE">UPDATE</option>
23
- <option value="DELETE">DELETE</option>
24
- <option value="CREATE">CREATE</option>
25
- <option value="ALTER">ALTER</option>
26
- <option value="DROP">DROP</option>
27
- </select>
28
- <button class="btn-clear-history" @click="confirmClearHistory">
29
- <i class="bi bi-trash"></i>
30
- <span>清空历史</span>
31
- </button>
32
- </div>
33
- </div>
34
-
35
- <div class="history-content">
36
- <!-- 统计信息 -->
37
- <div class="stats-section">
38
- <div class="stat-card">
39
- <div class="stat-icon">
40
- <i class="bi bi-clock"></i>
41
- </div>
42
- <div class="stat-info">
43
- <div class="stat-number">{{ totalQueries }}</div>
44
- <div class="stat-label">总查询数</div>
45
- </div>
46
- </div>
47
- <div class="stat-card">
48
- <div class="stat-icon">
49
- <i class="bi bi-lightning"></i>
50
- </div>
51
- <div class="stat-info">
52
- <div class="stat-number">{{ recentQueries.length }}</div>
53
- <div class="stat-label">今日查询</div>
54
- </div>
55
- </div>
56
- <div class="stat-card">
57
- <div class="stat-icon">
58
- <i class="bi bi-star"></i>
59
- </div>
60
- <div class="stat-info">
61
- <div class="stat-number">{{ favoriteQueries.length }}</div>
62
- <div class="stat-label">收藏查询</div>
63
- </div>
64
- </div>
65
- <div class="stat-card">
66
- <div class="stat-icon">
67
- <i class="bi bi-activity"></i>
68
- </div>
69
- <div class="stat-info">
70
- <div class="stat-number">{{ Math.round(averageExecutionTime) }}ms</div>
71
- <div class="stat-label">平均执行时间</div>
72
- </div>
73
- </div>
74
- </div>
75
-
76
- <!-- 快速筛选标签 -->
77
- <div class="quick-filters">
78
- <button
79
- class="filter-tag"
80
- :class="{ active: activeFilter === 'all' }"
81
- @click="setActiveFilter('all')"
82
- >
83
- <i class="bi bi-grid"></i>
84
- <span>全部</span>
85
- </button>
86
- <button
87
- class="filter-tag"
88
- :class="{ active: activeFilter === 'recent' }"
89
- @click="setActiveFilter('recent')"
90
- >
91
- <i class="bi bi-clock"></i>
92
- <span>最近</span>
93
- </button>
94
- <button
95
- class="filter-tag"
96
- :class="{ active: activeFilter === 'favorites' }"
97
- @click="setActiveFilter('favorites')"
98
- >
99
- <i class="bi bi-star"></i>
100
- <span>收藏</span>
101
- </button>
102
- <button
103
- class="filter-tag"
104
- :class="{ active: activeFilter === 'slow' }"
105
- @click="setActiveFilter('slow')"
106
- >
107
- <i class="bi bi-exclamation-triangle"></i>
108
- <span>慢查询</span>
109
- </button>
110
- <button
111
- class="filter-tag"
112
- :class="{ active: activeFilter === 'failed' }"
113
- @click="setActiveFilter('failed')"
114
- >
115
- <i class="bi bi-x-circle"></i>
116
- <span>失败</span>
117
- </button>
118
- </div>
119
-
120
- <!-- 查询列表 -->
121
- <div class="queries-list">
122
- <div class="list-header">
123
- <div class="list-title">
124
- <span>查询记录 ({{ filteredQueries.length }})</span>
125
- </div>
126
- <div class="list-actions">
127
- <button class="btn-export" @click="exportHistory">
128
- <i class="bi bi-download"></i>
129
- <span>导出</span>
130
- </button>
131
- </div>
132
- </div>
133
-
134
- <div class="query-items">
135
- <div
136
- v-for="query in paginatedQueries"
137
- :key="query.id"
138
- class="query-item"
139
- :class="{
140
- 'expanded': expandedQuery === query.id,
141
- 'failed': query.status === 'failed',
142
- 'slow': query.executionTime > 2000
143
- }"
144
- >
145
- <div class="query-header" @click="toggleExpand(query.id)">
146
- <div class="query-info">
147
- <div class="query-type-badge" :class="query.type.toLowerCase()">
148
- {{ query.type }}
149
- </div>
150
- <div class="query-summary">
151
- <span class="query-text">{{ truncateSql(query.sql) }}</span>
152
- </div>
153
- <div class="query-meta">
154
- <span class="execution-time" :class="getExecutionTimeClass(query.executionTime)">
155
- {{ query.executionTime }}ms
156
- </span>
157
- <span class="timestamp">{{ formatTime(query.timestamp) }}</span>
158
- <span class="connection">{{ query.connectionName }}</span>
159
- </div>
160
- </div>
161
- <div class="query-actions">
162
- <button
163
- class="btn-favorite"
164
- :class="{ 'active': query.isFavorite }"
165
- @click.stop="toggleFavorite(query)"
166
- :title="query.isFavorite ? '取消收藏' : '收藏查询'"
167
- >
168
- <i class="bi bi-star-fill" v-if="query.isFavorite"></i>
169
- <i class="bi bi-star" v-else></i>
170
- </button>
171
- <button
172
- class="btn-execute"
173
- @click.stop="executeQuery(query)"
174
- title="重新执行"
175
- >
176
- <i class="bi bi-play-fill"></i>
177
- </button>
178
- <button
179
- class="btn-copy"
180
- @click.stop="copyQuery(query)"
181
- title="复制SQL"
182
- >
183
- <i class="bi bi-clipboard"></i>
184
- </button>
185
- <button
186
- class="btn-delete"
187
- @click.stop="deleteQuery(query)"
188
- title="删除记录"
189
- >
190
- <i class="bi bi-trash"></i>
191
- </button>
192
- </div>
193
- </div>
194
-
195
- <div class="query-details" v-show="expandedQuery === query.id">
196
- <div class="details-tabs">
197
- <button
198
- class="tab-btn"
199
- :class="{ active: activeDetailTab === 'sql' }"
200
- @click="activeDetailTab = 'sql'"
201
- >
202
- <span>SQL语句</span>
203
- </button>
204
- <button
205
- class="tab-btn"
206
- :class="{ active: activeDetailTab === 'result' }"
207
- @click="activeDetailTab = 'result'"
208
- v-if="query.status === 'success'"
209
- >
210
- <span>执行结果</span>
211
- </button>
212
- <button
213
- class="tab-btn"
214
- :class="{ active: activeDetailTab === 'error' }"
215
- @click="activeDetailTab = 'error'"
216
- v-if="query.status === 'failed'"
217
- >
218
- <span>错误信息</span>
219
- </button>
220
- <button
221
- class="tab-btn"
222
- :class="{ active: activeDetailTab === 'plan' }"
223
- @click="activeDetailTab = 'plan'"
224
- >
225
- <span>执行计划</span>
226
- </button>
227
- </div>
228
-
229
- <div class="details-content">
230
- <div class="detail-panel" v-if="activeDetailTab === 'sql'">
231
- <div class="sql-editor">
232
- <pre><code>{{ query.sql }}</code></pre>
233
- <button class="btn-copy-sql" @click="copySql(query.sql)">
234
- <i class="bi bi-clipboard"></i>
235
- 复制
236
- </button>
237
- </div>
238
- </div>
239
-
240
- <div class="detail-panel" v-if="activeDetailTab === 'result' && query.status === 'success'">
241
- <div class="result-info">
242
- <div class="result-stat">
243
- <span class="stat-label">影响行数:</span>
244
- <span class="stat-value">{{ query.affectedRows || 0 }}</span>
245
- </div>
246
- <div class="result-stat">
247
- <span class="stat-label">返回行数:</span>
248
- <span class="stat-value">{{ query.returnedRows || 0 }}</span>
249
- </div>
250
- <div class="result-stat">
251
- <span class="stat-label">执行时间:</span>
252
- <span class="stat-value">{{ query.executionTime }}ms</span>
253
- </div>
254
- </div>
255
- <div class="result-preview" v-if="query.resultPreview">
256
- <table class="preview-table">
257
- <thead>
258
- <tr>
259
- <th v-for="column in query.resultPreview.columns" :key="column">
260
- {{ column }}
261
- </th>
262
- </tr>
263
- </thead>
264
- <tbody>
265
- <tr v-for="(row, index) in query.resultPreview.data" :key="index">
266
- <td v-for="column in query.resultPreview.columns" :key="column">
267
- {{ row[column] }}
268
- </td>
269
- </tr>
270
- </tbody>
271
- </table>
272
- </div>
273
- </div>
274
-
275
- <div class="detail-panel" v-if="activeDetailTab === 'error' && query.status === 'failed'">
276
- <div class="error-message">
277
- <i class="bi bi-exclamation-triangle"></i>
278
- <div class="error-content">
279
- <h4>执行错误</h4>
280
- <pre>{{ query.errorMessage }}</pre>
281
- </div>
282
- </div>
283
- </div>
284
-
285
- <div class="detail-panel" v-if="activeDetailTab === 'plan'">
286
- <div class="execution-plan">
287
- <pre>{{ query.executionPlan || '暂无执行计划数据' }}</pre>
288
- </div>
289
- </div>
290
- </div>
291
- </div>
292
- </div>
293
- </div>
294
- </div>
295
-
296
- <!-- 分页 -->
297
- <div class="pagination" v-if="totalPages > 1">
298
- <button @click="prevPage" :disabled="currentPage === 1">
299
- <i class="bi bi-chevron-left"></i>
300
- </button>
301
- <span class="page-info">{{ currentPage }} / {{ totalPages }}</span>
302
- <button @click="nextPage" :disabled="currentPage === totalPages">
303
- <i class="bi bi-chevron-right"></i>
304
- </button>
305
- </div>
306
- </div>
307
-
308
- <!-- 确认清空对话框 -->
309
- <div class="modal-overlay" v-if="showClearConfirm">
310
- <div class="modal-content">
311
- <div class="modal-header">
312
- <h3>确认清空</h3>
313
- <button class="modal-close" @click="showClearConfirm = false">
314
- <i class="bi bi-x-lg"></i>
315
- </button>
316
- </div>
317
- <div class="modal-body">
318
- <p>确定要清空所有查询历史记录吗?此操作无法撤销。</p>
319
- </div>
320
- <div class="modal-footer">
321
- <button class="btn-cancel" @click="showClearConfirm = false">取消</button>
322
- <button class="btn-confirm" @click="clearHistory">确认清空</button>
323
- </div>
324
- </div>
325
- </div>
326
- </div>
327
- </template>
328
-
329
- <script lang="ts" setup>
330
- import { ref, computed, onMounted } from 'vue';
331
-
332
- // 响应式数据
333
- const searchQuery = ref('');
334
- const filterType = ref('all');
335
- const activeFilter = ref('all');
336
- const expandedQuery = ref<string | null>(null);
337
- const activeDetailTab = ref('sql');
338
- const currentPage = ref(1);
339
- const pageSize = ref(20);
340
- const showClearConfirm = ref(false);
341
-
342
- // 模拟查询历史数据
343
- const queryHistory = ref([
344
- {
345
- id: '1',
346
- type: 'SELECT',
347
- sql: 'SELECT * FROM users WHERE status = "active" AND created_at > "2024-01-01"',
348
- timestamp: new Date(Date.now() - 5 * 60000),
349
- executionTime: 245,
350
- status: 'success',
351
- connectionName: 'MySQL主库',
352
- isFavorite: true,
353
- affectedRows: 0,
354
- returnedRows: 125,
355
- resultPreview: {
356
- columns: ['id', 'name', 'email', 'status'],
357
- data: [
358
- { id: 1, name: '张三', email: 'zhangsan@example.com', status: 'active' },
359
- { id: 2, name: '李四', email: 'lisi@example.com', status: 'active' }
360
- ]
361
- }
362
- },
363
- {
364
- id: '2',
365
- type: 'UPDATE',
366
- sql: 'UPDATE products SET price = price * 1.1 WHERE category_id = 5',
367
- timestamp: new Date(Date.now() - 15 * 60000),
368
- executionTime: 3200,
369
- status: 'success',
370
- connectionName: 'MySQL主库',
371
- isFavorite: false,
372
- affectedRows: 48,
373
- returnedRows: 0
374
- },
375
- {
376
- id: '3',
377
- type: 'INSERT',
378
- sql: 'INSERT INTO orders (user_id, product_id, quantity, total_amount) VALUES (1, 10, 2, 299.99)',
379
- timestamp: new Date(Date.now() - 30 * 60000),
380
- executionTime: 120,
381
- status: 'success',
382
- connectionName: 'MySQL主库',
383
- isFavorite: false,
384
- affectedRows: 1,
385
- returnedRows: 0
386
- },
387
- {
388
- id: '4',
389
- type: 'SELECT',
390
- sql: 'SELECT u.name, COUNT(o.id) as order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.created_at > "2024-01-01" GROUP BY u.id, u.name HAVING COUNT(o.id) > 5',
391
- timestamp: new Date(Date.now() - 60 * 60000),
392
- executionTime: 5600,
393
- status: 'success',
394
- connectionName: 'MySQL主库',
395
- isFavorite: true,
396
- affectedRows: 0,
397
- returnedRows: 23
398
- },
399
- {
400
- id: '5',
401
- type: 'DELETE',
402
- sql: 'DELETE FROM user_sessions WHERE last_activity < DATE_SUB(NOW(), INTERVAL 30 DAY)',
403
- timestamp: new Date(Date.now() - 120 * 60000),
404
- executionTime: 1850,
405
- status: 'failed',
406
- connectionName: 'MySQL主库',
407
- isFavorite: false,
408
- errorMessage: 'Error 1451: Cannot delete or update a parent row: a foreign key constraint fails'
409
- }
410
- ]);
411
-
412
- // 计算属性
413
- const totalQueries = computed(() => queryHistory.value.length);
414
-
415
- const recentQueries = computed(() => {
416
- const today = new Date();
417
- today.setHours(0, 0, 0, 0);
418
- return queryHistory.value.filter(q => q.timestamp >= today);
419
- });
420
-
421
- const favoriteQueries = computed(() =>
422
- queryHistory.value.filter(q => q.isFavorite)
423
- );
424
-
425
- const averageExecutionTime = computed(() => {
426
- const successfulQueries = queryHistory.value.filter(q => q.status === 'success');
427
- if (successfulQueries.length === 0) return 0;
428
- const total = successfulQueries.reduce((sum, q) => sum + q.executionTime, 0);
429
- return total / successfulQueries.length;
430
- });
431
-
432
- const filteredQueries = computed(() => {
433
- let filtered = queryHistory.value;
434
-
435
- // 搜索过滤
436
- if (searchQuery.value) {
437
- const query = searchQuery.value.toLowerCase();
438
- filtered = filtered.filter(q =>
439
- q.sql.toLowerCase().includes(query) ||
440
- q.connectionName.toLowerCase().includes(query)
441
- );
442
- }
443
-
444
- // 类型过滤
445
- if (filterType.value !== 'all') {
446
- filtered = filtered.filter(q => q.type === filterType.value);
447
- }
448
-
449
- // 快速筛选
450
- switch (activeFilter.value) {
451
- case 'recent':
452
- const today = new Date();
453
- today.setHours(0, 0, 0, 0);
454
- filtered = filtered.filter(q => q.timestamp >= today);
455
- break;
456
- case 'favorites':
457
- filtered = filtered.filter(q => q.isFavorite);
458
- break;
459
- case 'slow':
460
- filtered = filtered.filter(q => q.executionTime > 2000);
461
- break;
462
- case 'failed':
463
- filtered = filtered.filter(q => q.status === 'failed');
464
- break;
465
- }
466
-
467
- // 按时间倒序
468
- return filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
469
- });
470
-
471
- const totalPages = computed(() =>
472
- Math.ceil(filteredQueries.value.length / pageSize.value)
473
- );
474
-
475
- const paginatedQueries = computed(() => {
476
- const start = (currentPage.value - 1) * pageSize.value;
477
- const end = start + pageSize.value;
478
- return filteredQueries.value.slice(start, end);
479
- });
480
-
481
- // 方法
482
- function handleSearch() {
483
- currentPage.value = 1;
484
- }
485
-
486
- function filterQueries() {
487
- currentPage.value = 1;
488
- }
489
-
490
- function setActiveFilter(filter: string) {
491
- activeFilter.value = filter;
492
- currentPage.value = 1;
493
- }
494
-
495
- function toggleExpand(queryId: string) {
496
- expandedQuery.value = expandedQuery.value === queryId ? null : queryId;
497
- activeDetailTab.value = 'sql';
498
- }
499
-
500
- function toggleFavorite(query: any) {
501
- query.isFavorite = !query.isFavorite;
502
- // 这里可以调用API更新收藏状态
503
- }
504
-
505
- function executeQuery(query: any) {
506
- console.log('执行查询:', query);
507
- // 这里应该跳转到SQL编辑器并加载查询
508
- }
509
-
510
- function copyQuery(query: any) {
511
- copySql(query.sql);
512
- }
513
-
514
- function copySql(sql: string) {
515
- navigator.clipboard.writeText(sql).then(() => {
516
- // 这里可以显示复制成功提示
517
- console.log('SQL已复制到剪贴板');
518
- });
519
- }
520
-
521
- function deleteQuery(query: any) {
522
- const index = queryHistory.value.findIndex(q => q.id === query.id);
523
- if (index > -1) {
524
- queryHistory.value.splice(index, 1);
525
- // 这里可以调用API删除记录
526
- }
527
- }
528
-
529
- function truncateSql(sql: string, maxLength = 80): string {
530
- if (sql.length <= maxLength) return sql;
531
- return sql.substring(0, maxLength) + '...';
532
- }
533
-
534
- function formatTime(timestamp: Date): string {
535
- const now = new Date();
536
- const diff = now.getTime() - timestamp.getTime();
537
- const minutes = Math.floor(diff / 60000);
538
- const hours = Math.floor(diff / 3600000);
539
- const days = Math.floor(diff / 86400000);
540
-
541
- if (minutes < 1) return '刚刚';
542
- if (minutes < 60) return `${minutes}分钟前`;
543
- if (hours < 24) return `${hours}小时前`;
544
- if (days < 7) return `${days}天前`;
545
-
546
- return timestamp.toLocaleDateString('zh-CN');
547
- }
548
-
549
- function getExecutionTimeClass(time: number): string {
550
- if (time < 100) return 'fast';
551
- if (time < 1000) return 'normal';
552
- if (time < 2000) return 'slow';
553
- return 'very-slow';
554
- }
555
-
556
- function confirmClearHistory() {
557
- showClearConfirm.value = true;
558
- }
559
-
560
- function clearHistory() {
561
- queryHistory.value = [];
562
- showClearConfirm.value = false;
563
- currentPage.value = 1;
564
- }
565
-
566
- function exportHistory() {
567
- const data = filteredQueries.value.map(q => ({
568
- 时间: q.timestamp.toLocaleString('zh-CN'),
569
- 类型: q.type,
570
- 连接: q.connectionName,
571
- 执行时间: q.executionTime + 'ms',
572
- 状态: q.status,
573
- SQL: q.sql
574
- }));
575
-
576
- const csv = [
577
- Object.keys(data[0]).join(','),
578
- ...data.map(row => Object.values(row).map(v => `"${v}"`).join(','))
579
- ].join('\n');
580
-
581
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
582
- const link = document.createElement('a');
583
- link.href = URL.createObjectURL(blob);
584
- link.download = `query_history_${new Date().getTime()}.csv`;
585
- link.click();
586
- }
587
-
588
- function nextPage() {
589
- if (currentPage.value < totalPages.value) {
590
- currentPage.value++;
591
- }
592
- }
593
-
594
- function prevPage() {
595
- if (currentPage.value > 1) {
596
- currentPage.value--;
597
- }
598
- }
599
-
600
- // 生命周期
601
- onMounted(() => {
602
- // 加载查询历史数据
603
- });
604
- </script>
605
-
606
- <style scoped>
607
- .query-history {
608
- height: 100%;
609
- display: flex;
610
- flex-direction: column;
611
- background: white;
612
- border-radius: 12px;
613
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
614
- overflow: hidden;
615
- }
616
-
617
- .history-header {
618
- display: flex;
619
- justify-content: space-between;
620
- align-items: center;
621
- padding: 1rem 1.5rem;
622
- background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
623
- border-bottom: 1px solid #e2e8f0;
624
- }
625
-
626
- .header-title {
627
- display: flex;
628
- align-items: center;
629
- gap: 0.5rem;
630
- font-weight: 600;
631
- color: #1e293b;
632
- font-size: 1.1rem;
633
- }
634
-
635
- .header-controls {
636
- display: flex;
637
- align-items: center;
638
- gap: 1rem;
639
- }
640
-
641
- .search-box {
642
- position: relative;
643
- display: flex;
644
- align-items: center;
645
- }
646
-
647
- .search-box i {
648
- position: absolute;
649
- left: 0.75rem;
650
- color: #6b7280;
651
- font-size: 0.875rem;
652
- }
653
-
654
- .search-box input {
655
- padding: 0.5rem 0.75rem 0.5rem 2.25rem;
656
- border: 1px solid #d1d5db;
657
- border-radius: 8px;
658
- font-size: 0.875rem;
659
- width: 250px;
660
- }
661
-
662
- .search-box input:focus {
663
- outline: none;
664
- border-color: #667eea;
665
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
666
- }
667
-
668
- .filter-select {
669
- padding: 0.5rem 0.75rem;
670
- border: 1px solid #d1d5db;
671
- border-radius: 8px;
672
- background: white;
673
- font-size: 0.875rem;
674
- }
675
-
676
- .btn-clear-history {
677
- display: flex;
678
- align-items: center;
679
- gap: 0.25rem;
680
- padding: 0.5rem 1rem;
681
- border: 1px solid #ef4444;
682
- border-radius: 8px;
683
- background: white;
684
- color: #ef4444;
685
- font-size: 0.875rem;
686
- cursor: pointer;
687
- transition: all 0.2s ease;
688
- }
689
-
690
- .btn-clear-history:hover {
691
- background: #ef4444;
692
- color: white;
693
- }
694
-
695
- .history-content {
696
- flex: 1;
697
- overflow-y: auto;
698
- padding: 1.5rem;
699
- }
700
-
701
- /* 统计卡片 */
702
- .stats-section {
703
- display: grid;
704
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
705
- gap: 1rem;
706
- margin-bottom: 2rem;
707
- }
708
-
709
- .stat-card {
710
- display: flex;
711
- align-items: center;
712
- gap: 1rem;
713
- padding: 1.25rem;
714
- background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
715
- border: 1px solid #e2e8f0;
716
- border-radius: 12px;
717
- transition: all 0.2s ease;
718
- }
719
-
720
- .stat-card:hover {
721
- transform: translateY(-2px);
722
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
723
- }
724
-
725
- .stat-icon {
726
- width: 48px;
727
- height: 48px;
728
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
729
- border-radius: 12px;
730
- display: flex;
731
- align-items: center;
732
- justify-content: center;
733
- color: white;
734
- font-size: 1.25rem;
735
- }
736
-
737
- .stat-info {
738
- flex: 1;
739
- }
740
-
741
- .stat-number {
742
- font-size: 1.5rem;
743
- font-weight: 700;
744
- color: #1e293b;
745
- margin-bottom: 0.25rem;
746
- }
747
-
748
- .stat-label {
749
- font-size: 0.875rem;
750
- color: #64748b;
751
- }
752
-
753
- /* 快速筛选 */
754
- .quick-filters {
755
- display: flex;
756
- gap: 0.5rem;
757
- margin-bottom: 2rem;
758
- flex-wrap: wrap;
759
- }
760
-
761
- .filter-tag {
762
- display: flex;
763
- align-items: center;
764
- gap: 0.5rem;
765
- padding: 0.5rem 1rem;
766
- border: 1px solid #d1d5db;
767
- border-radius: 20px;
768
- background: white;
769
- color: #6b7280;
770
- font-size: 0.875rem;
771
- cursor: pointer;
772
- transition: all 0.2s ease;
773
- }
774
-
775
- .filter-tag:hover {
776
- background: #f3f4f6;
777
- border-color: #9ca3af;
778
- }
779
-
780
- .filter-tag.active {
781
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
782
- color: white;
783
- border-color: transparent;
784
- }
785
-
786
- /* 查询列表 */
787
- .queries-list {
788
- flex: 1;
789
- }
790
-
791
- .list-header {
792
- display: flex;
793
- justify-content: space-between;
794
- align-items: center;
795
- margin-bottom: 1rem;
796
- }
797
-
798
- .list-title {
799
- font-size: 1rem;
800
- font-weight: 600;
801
- color: #1e293b;
802
- }
803
-
804
- .list-actions {
805
- display: flex;
806
- gap: 0.5rem;
807
- }
808
-
809
- .btn-export {
810
- display: flex;
811
- align-items: center;
812
- gap: 0.25rem;
813
- padding: 0.5rem 1rem;
814
- border: 1px solid #667eea;
815
- border-radius: 8px;
816
- background: white;
817
- color: #667eea;
818
- font-size: 0.875rem;
819
- cursor: pointer;
820
- transition: all 0.2s ease;
821
- }
822
-
823
- .btn-export:hover {
824
- background: #667eea;
825
- color: white;
826
- }
827
-
828
- .query-items {
829
- display: flex;
830
- flex-direction: column;
831
- gap: 0.75rem;
832
- }
833
-
834
- .query-item {
835
- border: 1px solid #e5e7eb;
836
- border-radius: 12px;
837
- overflow: hidden;
838
- transition: all 0.2s ease;
839
- }
840
-
841
- .query-item:hover {
842
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
843
- }
844
-
845
- .query-item.failed {
846
- border-left: 4px solid #ef4444;
847
- }
848
-
849
- .query-item.slow {
850
- border-left: 4px solid #f59e0b;
851
- }
852
-
853
- .query-item.expanded {
854
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
855
- }
856
-
857
- .query-header {
858
- display: flex;
859
- justify-content: space-between;
860
- align-items: center;
861
- padding: 1rem 1.25rem;
862
- cursor: pointer;
863
- transition: background-color 0.2s ease;
864
- }
865
-
866
- .query-header:hover {
867
- background: #f8fafc;
868
- }
869
-
870
- .query-info {
871
- flex: 1;
872
- display: flex;
873
- align-items: center;
874
- gap: 1rem;
875
- min-width: 0;
876
- }
877
-
878
- .query-type-badge {
879
- padding: 0.25rem 0.75rem;
880
- border-radius: 20px;
881
- font-size: 0.75rem;
882
- font-weight: 600;
883
- color: white;
884
- white-space: nowrap;
885
- }
886
-
887
- .query-type-badge.select { background: #3b82f6; }
888
- .query-type-badge.insert { background: #10b981; }
889
- .query-type-badge.update { background: #f59e0b; }
890
- .query-type-badge.delete { background: #ef4444; }
891
- .query-type-badge.create { background: #8b5cf6; }
892
- .query-type-badge.alter { background: #06b6d4; }
893
- .query-type-badge.drop { background: #dc2626; }
894
-
895
- .query-summary {
896
- flex: 1;
897
- min-width: 0;
898
- }
899
-
900
- .query-text {
901
- font-family: 'Consolas', 'Monaco', monospace;
902
- font-size: 0.875rem;
903
- color: #374151;
904
- display: block;
905
- }
906
-
907
- .query-meta {
908
- display: flex;
909
- align-items: center;
910
- gap: 1rem;
911
- font-size: 0.75rem;
912
- color: #6b7280;
913
- }
914
-
915
- .execution-time {
916
- font-weight: 600;
917
- padding: 0.125rem 0.5rem;
918
- border-radius: 4px;
919
- }
920
-
921
- .execution-time.fast { color: #10b981; background: #d1fae5; }
922
- .execution-time.normal { color: #3b82f6; background: #dbeafe; }
923
- .execution-time.slow { color: #f59e0b; background: #fef3c7; }
924
- .execution-time.very-slow { color: #ef4444; background: #fee2e2; }
925
-
926
- .query-actions {
927
- display: flex;
928
- align-items: center;
929
- gap: 0.25rem;
930
- }
931
-
932
- .query-actions button {
933
- width: 32px;
934
- height: 32px;
935
- border: 1px solid #e5e7eb;
936
- background: white;
937
- border-radius: 6px;
938
- display: flex;
939
- align-items: center;
940
- justify-content: center;
941
- cursor: pointer;
942
- transition: all 0.2s ease;
943
- }
944
-
945
- .query-actions button:hover {
946
- background: #f3f4f6;
947
- border-color: #d1d5db;
948
- }
949
-
950
- .btn-favorite.active {
951
- background: #fbbf24;
952
- border-color: #fbbf24;
953
- color: white;
954
- }
955
-
956
- .btn-execute:hover {
957
- background: #10b981;
958
- border-color: #10b981;
959
- color: white;
960
- }
961
-
962
- .btn-copy:hover {
963
- background: #3b82f6;
964
- border-color: #3b82f6;
965
- color: white;
966
- }
967
-
968
- .btn-delete:hover {
969
- background: #ef4444;
970
- border-color: #ef4444;
971
- color: white;
972
- }
973
-
974
- .query-details {
975
- border-top: 1px solid #e5e7eb;
976
- background: #fafbfc;
977
- }
978
-
979
- .details-tabs {
980
- display: flex;
981
- border-bottom: 1px solid #e5e7eb;
982
- }
983
-
984
- .tab-btn {
985
- padding: 0.75rem 1.5rem;
986
- background: none;
987
- border: none;
988
- color: #6b7280;
989
- font-size: 0.875rem;
990
- cursor: pointer;
991
- transition: all 0.2s ease;
992
- border-bottom: 2px solid transparent;
993
- }
994
-
995
- .tab-btn:hover {
996
- color: #374151;
997
- }
998
-
999
- .tab-btn.active {
1000
- color: #667eea;
1001
- border-bottom-color: #667eea;
1002
- background: white;
1003
- }
1004
-
1005
- .details-content {
1006
- padding: 1.25rem;
1007
- }
1008
-
1009
- .detail-panel {
1010
- min-height: 100px;
1011
- }
1012
-
1013
- .sql-editor {
1014
- position: relative;
1015
- background: #1e293b;
1016
- border-radius: 8px;
1017
- padding: 1rem;
1018
- overflow-x: auto;
1019
- }
1020
-
1021
- .sql-editor pre {
1022
- margin: 0;
1023
- color: #e2e8f0;
1024
- font-family: 'Consolas', 'Monaco', monospace;
1025
- font-size: 0.875rem;
1026
- line-height: 1.5;
1027
- }
1028
-
1029
- .btn-copy-sql {
1030
- position: absolute;
1031
- top: 0.5rem;
1032
- right: 0.5rem;
1033
- padding: 0.25rem 0.5rem;
1034
- background: rgba(255, 255, 255, 0.1);
1035
- border: 1px solid rgba(255, 255, 255, 0.2);
1036
- border-radius: 4px;
1037
- color: white;
1038
- font-size: 0.75rem;
1039
- cursor: pointer;
1040
- transition: all 0.2s ease;
1041
- }
1042
-
1043
- .btn-copy-sql:hover {
1044
- background: rgba(255, 255, 255, 0.2);
1045
- }
1046
-
1047
- .result-info {
1048
- display: grid;
1049
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1050
- gap: 1rem;
1051
- margin-bottom: 1.5rem;
1052
- }
1053
-
1054
- .result-stat {
1055
- display: flex;
1056
- flex-direction: column;
1057
- gap: 0.25rem;
1058
- }
1059
-
1060
- .stat-label {
1061
- font-size: 0.875rem;
1062
- color: #6b7280;
1063
- }
1064
-
1065
- .stat-value {
1066
- font-size: 1.125rem;
1067
- font-weight: 600;
1068
- color: #1e293b;
1069
- }
1070
-
1071
- .result-preview {
1072
- border: 1px solid #e5e7eb;
1073
- border-radius: 8px;
1074
- overflow: hidden;
1075
- }
1076
-
1077
- .preview-table {
1078
- width: 100%;
1079
- border-collapse: collapse;
1080
- font-size: 0.875rem;
1081
- }
1082
-
1083
- .preview-table th {
1084
- background: #f8fafc;
1085
- padding: 0.75rem;
1086
- text-align: left;
1087
- font-weight: 600;
1088
- color: #374151;
1089
- border-bottom: 1px solid #e5e7eb;
1090
- }
1091
-
1092
- .preview-table td {
1093
- padding: 0.75rem;
1094
- border-bottom: 1px solid #f3f4f6;
1095
- }
1096
-
1097
- .error-message {
1098
- display: flex;
1099
- align-items: flex-start;
1100
- gap: 1rem;
1101
- padding: 1rem;
1102
- background: #fef2f2;
1103
- border: 1px solid #fecaca;
1104
- border-radius: 8px;
1105
- color: #991b1b;
1106
- }
1107
-
1108
- .error-message i {
1109
- font-size: 1.25rem;
1110
- color: #ef4444;
1111
- margin-top: 0.125rem;
1112
- }
1113
-
1114
- .error-content h4 {
1115
- margin: 0 0 0.5rem 0;
1116
- font-size: 1rem;
1117
- }
1118
-
1119
- .error-content pre {
1120
- margin: 0;
1121
- background: #fee2e2;
1122
- padding: 0.75rem;
1123
- border-radius: 4px;
1124
- font-family: 'Consolas', 'Monaco', monospace;
1125
- font-size: 0.875rem;
1126
- }
1127
-
1128
- .execution-plan {
1129
- background: #f8fafc;
1130
- border: 1px solid #e5e7eb;
1131
- border-radius: 8px;
1132
- padding: 1rem;
1133
- }
1134
-
1135
- .execution-plan pre {
1136
- margin: 0;
1137
- font-family: 'Consolas', 'Monaco', monospace;
1138
- font-size: 0.875rem;
1139
- color: #374151;
1140
- }
1141
-
1142
- /* 分页 */
1143
- .pagination {
1144
- display: flex;
1145
- justify-content: center;
1146
- align-items: center;
1147
- gap: 0.5rem;
1148
- margin-top: 2rem;
1149
- padding: 1rem;
1150
- background: #f8fafc;
1151
- border-radius: 8px;
1152
- }
1153
-
1154
- .pagination button {
1155
- padding: 0.5rem 0.75rem;
1156
- border: 1px solid #d1d5db;
1157
- border-radius: 6px;
1158
- background: white;
1159
- cursor: pointer;
1160
- transition: all 0.2s ease;
1161
- }
1162
-
1163
- .pagination button:hover:not(:disabled) {
1164
- background: #667eea;
1165
- color: white;
1166
- border-color: #667eea;
1167
- }
1168
-
1169
- .pagination button:disabled {
1170
- opacity: 0.5;
1171
- cursor: not-allowed;
1172
- }
1173
-
1174
- .page-info {
1175
- padding: 0.5rem 1rem;
1176
- font-size: 0.875rem;
1177
- color: #6b7280;
1178
- }
1179
-
1180
- /* 模态框 */
1181
- .modal-overlay {
1182
- position: fixed;
1183
- top: 0;
1184
- left: 0;
1185
- right: 0;
1186
- bottom: 0;
1187
- background: rgba(0, 0, 0, 0.5);
1188
- display: flex;
1189
- justify-content: center;
1190
- align-items: center;
1191
- z-index: 1000;
1192
- }
1193
-
1194
- .modal-content {
1195
- background: white;
1196
- border-radius: 12px;
1197
- width: 90%;
1198
- max-width: 400px;
1199
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
1200
- }
1201
-
1202
- .modal-header {
1203
- display: flex;
1204
- justify-content: space-between;
1205
- align-items: center;
1206
- padding: 1.5rem;
1207
- border-bottom: 1px solid #e5e7eb;
1208
- }
1209
-
1210
- .modal-header h3 {
1211
- margin: 0;
1212
- font-size: 1.25rem;
1213
- font-weight: 600;
1214
- color: #1e293b;
1215
- }
1216
-
1217
- .modal-close {
1218
- background: none;
1219
- border: none;
1220
- font-size: 1.25rem;
1221
- cursor: pointer;
1222
- color: #6b7280;
1223
- padding: 0.25rem;
1224
- }
1225
-
1226
- .modal-close:hover {
1227
- color: #374151;
1228
- }
1229
-
1230
- .modal-body {
1231
- padding: 1.5rem;
1232
- }
1233
-
1234
- .modal-body p {
1235
- margin: 0;
1236
- color: #374151;
1237
- line-height: 1.5;
1238
- }
1239
-
1240
- .modal-footer {
1241
- display: flex;
1242
- justify-content: flex-end;
1243
- gap: 0.5rem;
1244
- padding: 1.5rem;
1245
- border-top: 1px solid #e5e7eb;
1246
- }
1247
-
1248
- .btn-cancel,
1249
- .btn-confirm {
1250
- padding: 0.75rem 1.5rem;
1251
- border-radius: 8px;
1252
- font-weight: 500;
1253
- cursor: pointer;
1254
- transition: all 0.2s ease;
1255
- }
1256
-
1257
- .btn-cancel {
1258
- background: white;
1259
- border: 1px solid #d1d5db;
1260
- color: #374151;
1261
- }
1262
-
1263
- .btn-cancel:hover {
1264
- background: #f3f4f6;
1265
- }
1266
-
1267
- .btn-confirm {
1268
- background: #ef4444;
1269
- border: none;
1270
- color: white;
1271
- }
1272
-
1273
- .btn-confirm:hover {
1274
- background: #dc2626;
1275
- }
1276
-
1277
- /* 响应式设计 */
1278
- @media (max-width: 768px) {
1279
- .history-header {
1280
- flex-direction: column;
1281
- gap: 1rem;
1282
- align-items: stretch;
1283
- }
1284
-
1285
- .header-controls {
1286
- flex-wrap: wrap;
1287
- }
1288
-
1289
- .search-box input {
1290
- width: 100%;
1291
- }
1292
-
1293
- .stats-section {
1294
- grid-template-columns: repeat(2, 1fr);
1295
- }
1296
-
1297
- .quick-filters {
1298
- justify-content: center;
1299
- }
1300
-
1301
- .query-header {
1302
- flex-direction: column;
1303
- gap: 1rem;
1304
- align-items: stretch;
1305
- }
1306
-
1307
- .query-info {
1308
- flex-direction: column;
1309
- align-items: stretch;
1310
- gap: 0.75rem;
1311
- }
1312
-
1313
- .query-meta {
1314
- justify-content: space-between;
1315
- }
1316
-
1317
- .query-actions {
1318
- justify-content: center;
1319
- }
1320
-
1321
- .details-tabs {
1322
- overflow-x: auto;
1323
- }
1324
-
1325
- .tab-btn {
1326
- flex-shrink: 0;
1327
- }
1328
- }
1329
-
1330
- @media (max-width: 480px) {
1331
- .history-content {
1332
- padding: 1rem;
1333
- }
1334
-
1335
- .stats-section {
1336
- grid-template-columns: 1fr;
1337
- }
1338
-
1339
- .list-header {
1340
- flex-direction: column;
1341
- gap: 1rem;
1342
- align-items: stretch;
1343
- }
1344
-
1345
- .result-info {
1346
- grid-template-columns: 1fr;
1347
- }
1348
- }
1
+ <template>
2
+ <div class="query-history">
3
+ <div class="history-header">
4
+ <div class="header-title">
5
+ <i class="bi bi-clock-history"></i>
6
+ <span>查询历史</span>
7
+ </div>
8
+ <div class="header-controls">
9
+ <div class="search-box">
10
+ <i class="bi bi-search"></i>
11
+ <input
12
+ type="text"
13
+ v-model="searchQuery"
14
+ placeholder="搜索查询..."
15
+ @input="handleSearch"
16
+ >
17
+ </div>
18
+ <select v-model="filterType" class="filter-select" @change="filterQueries">
19
+ <option value="all">所有类型</option>
20
+ <option value="SELECT">SELECT</option>
21
+ <option value="INSERT">INSERT</option>
22
+ <option value="UPDATE">UPDATE</option>
23
+ <option value="DELETE">DELETE</option>
24
+ <option value="CREATE">CREATE</option>
25
+ <option value="ALTER">ALTER</option>
26
+ <option value="DROP">DROP</option>
27
+ </select>
28
+ <button class="btn-clear-history" @click="confirmClearHistory">
29
+ <i class="bi bi-trash"></i>
30
+ <span>清空历史</span>
31
+ </button>
32
+ </div>
33
+ </div>
34
+
35
+ <div class="history-content">
36
+ <!-- 统计信息 -->
37
+ <div class="stats-section">
38
+ <div class="stat-card">
39
+ <div class="stat-icon">
40
+ <i class="bi bi-clock"></i>
41
+ </div>
42
+ <div class="stat-info">
43
+ <div class="stat-number">{{ totalQueries }}</div>
44
+ <div class="stat-label">总查询数</div>
45
+ </div>
46
+ </div>
47
+ <div class="stat-card">
48
+ <div class="stat-icon">
49
+ <i class="bi bi-lightning"></i>
50
+ </div>
51
+ <div class="stat-info">
52
+ <div class="stat-number">{{ recentQueries.length }}</div>
53
+ <div class="stat-label">今日查询</div>
54
+ </div>
55
+ </div>
56
+ <div class="stat-card">
57
+ <div class="stat-icon">
58
+ <i class="bi bi-star"></i>
59
+ </div>
60
+ <div class="stat-info">
61
+ <div class="stat-number">{{ favoriteQueries.length }}</div>
62
+ <div class="stat-label">收藏查询</div>
63
+ </div>
64
+ </div>
65
+ <div class="stat-card">
66
+ <div class="stat-icon">
67
+ <i class="bi bi-activity"></i>
68
+ </div>
69
+ <div class="stat-info">
70
+ <div class="stat-number">{{ Math.round(averageExecutionTime) }}ms</div>
71
+ <div class="stat-label">平均执行时间</div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- 快速筛选标签 -->
77
+ <div class="quick-filters">
78
+ <button
79
+ class="filter-tag"
80
+ :class="{ active: activeFilter === 'all' }"
81
+ @click="setActiveFilter('all')"
82
+ >
83
+ <i class="bi bi-grid"></i>
84
+ <span>全部</span>
85
+ </button>
86
+ <button
87
+ class="filter-tag"
88
+ :class="{ active: activeFilter === 'recent' }"
89
+ @click="setActiveFilter('recent')"
90
+ >
91
+ <i class="bi bi-clock"></i>
92
+ <span>最近</span>
93
+ </button>
94
+ <button
95
+ class="filter-tag"
96
+ :class="{ active: activeFilter === 'favorites' }"
97
+ @click="setActiveFilter('favorites')"
98
+ >
99
+ <i class="bi bi-star"></i>
100
+ <span>收藏</span>
101
+ </button>
102
+ <button
103
+ class="filter-tag"
104
+ :class="{ active: activeFilter === 'slow' }"
105
+ @click="setActiveFilter('slow')"
106
+ >
107
+ <i class="bi bi-exclamation-triangle"></i>
108
+ <span>慢查询</span>
109
+ </button>
110
+ <button
111
+ class="filter-tag"
112
+ :class="{ active: activeFilter === 'failed' }"
113
+ @click="setActiveFilter('failed')"
114
+ >
115
+ <i class="bi bi-x-circle"></i>
116
+ <span>失败</span>
117
+ </button>
118
+ </div>
119
+
120
+ <!-- 查询列表 -->
121
+ <div class="queries-list">
122
+ <div class="list-header">
123
+ <div class="list-title">
124
+ <span>查询记录 ({{ filteredQueries.length }})</span>
125
+ </div>
126
+ <div class="list-actions">
127
+ <button class="btn-export" @click="exportHistory">
128
+ <i class="bi bi-download"></i>
129
+ <span>导出</span>
130
+ </button>
131
+ </div>
132
+ </div>
133
+
134
+ <div class="query-items">
135
+ <div
136
+ v-for="query in paginatedQueries"
137
+ :key="query.id"
138
+ class="query-item"
139
+ :class="{
140
+ 'expanded': expandedQuery === query.id,
141
+ 'failed': query.status === 'failed',
142
+ 'slow': query.executionTime > 2000
143
+ }"
144
+ >
145
+ <div class="query-header" @click="toggleExpand(query.id)">
146
+ <div class="query-info">
147
+ <div class="query-type-badge" :class="query.type.toLowerCase()">
148
+ {{ query.type }}
149
+ </div>
150
+ <div class="query-summary">
151
+ <span class="query-text">{{ truncateSql(query.sql) }}</span>
152
+ </div>
153
+ <div class="query-meta">
154
+ <span class="execution-time" :class="getExecutionTimeClass(query.executionTime)">
155
+ {{ query.executionTime }}ms
156
+ </span>
157
+ <span class="timestamp">{{ formatTime(query.timestamp) }}</span>
158
+ <span class="connection">{{ query.connectionName }}</span>
159
+ </div>
160
+ </div>
161
+ <div class="query-actions">
162
+ <button
163
+ class="btn-favorite"
164
+ :class="{ 'active': query.isFavorite }"
165
+ @click.stop="toggleFavorite(query)"
166
+ :title="query.isFavorite ? '取消收藏' : '收藏查询'"
167
+ >
168
+ <i class="bi bi-star-fill" v-if="query.isFavorite"></i>
169
+ <i class="bi bi-star" v-else></i>
170
+ </button>
171
+ <button
172
+ class="btn-execute"
173
+ @click.stop="executeQuery(query)"
174
+ title="重新执行"
175
+ >
176
+ <i class="bi bi-play-fill"></i>
177
+ </button>
178
+ <button
179
+ class="btn-copy"
180
+ @click.stop="copyQuery(query)"
181
+ title="复制SQL"
182
+ >
183
+ <i class="bi bi-clipboard"></i>
184
+ </button>
185
+ <button
186
+ class="btn-delete"
187
+ @click.stop="deleteQuery(query)"
188
+ title="删除记录"
189
+ >
190
+ <i class="bi bi-trash"></i>
191
+ </button>
192
+ </div>
193
+ </div>
194
+
195
+ <div class="query-details" v-show="expandedQuery === query.id">
196
+ <div class="details-tabs">
197
+ <button
198
+ class="tab-btn"
199
+ :class="{ active: activeDetailTab === 'sql' }"
200
+ @click="activeDetailTab = 'sql'"
201
+ >
202
+ <span>SQL语句</span>
203
+ </button>
204
+ <button
205
+ class="tab-btn"
206
+ :class="{ active: activeDetailTab === 'result' }"
207
+ @click="activeDetailTab = 'result'"
208
+ v-if="query.status === 'success'"
209
+ >
210
+ <span>执行结果</span>
211
+ </button>
212
+ <button
213
+ class="tab-btn"
214
+ :class="{ active: activeDetailTab === 'error' }"
215
+ @click="activeDetailTab = 'error'"
216
+ v-if="query.status === 'failed'"
217
+ >
218
+ <span>错误信息</span>
219
+ </button>
220
+ <button
221
+ class="tab-btn"
222
+ :class="{ active: activeDetailTab === 'plan' }"
223
+ @click="activeDetailTab = 'plan'"
224
+ >
225
+ <span>执行计划</span>
226
+ </button>
227
+ </div>
228
+
229
+ <div class="details-content">
230
+ <div class="detail-panel" v-if="activeDetailTab === 'sql'">
231
+ <div class="sql-editor">
232
+ <pre><code>{{ query.sql }}</code></pre>
233
+ <button class="btn-copy-sql" @click="copySql(query.sql)">
234
+ <i class="bi bi-clipboard"></i>
235
+ 复制
236
+ </button>
237
+ </div>
238
+ </div>
239
+
240
+ <div class="detail-panel" v-if="activeDetailTab === 'result' && query.status === 'success'">
241
+ <div class="result-info">
242
+ <div class="result-stat">
243
+ <span class="stat-label">影响行数:</span>
244
+ <span class="stat-value">{{ query.affectedRows || 0 }}</span>
245
+ </div>
246
+ <div class="result-stat">
247
+ <span class="stat-label">返回行数:</span>
248
+ <span class="stat-value">{{ query.returnedRows || 0 }}</span>
249
+ </div>
250
+ <div class="result-stat">
251
+ <span class="stat-label">执行时间:</span>
252
+ <span class="stat-value">{{ query.executionTime }}ms</span>
253
+ </div>
254
+ </div>
255
+ <div class="result-preview" v-if="query.resultPreview">
256
+ <table class="preview-table">
257
+ <thead>
258
+ <tr>
259
+ <th v-for="column in query.resultPreview.columns" :key="column">
260
+ {{ column }}
261
+ </th>
262
+ </tr>
263
+ </thead>
264
+ <tbody>
265
+ <tr v-for="(row, index) in query.resultPreview.data" :key="index">
266
+ <td v-for="column in query.resultPreview.columns" :key="column">
267
+ {{ row[column] }}
268
+ </td>
269
+ </tr>
270
+ </tbody>
271
+ </table>
272
+ </div>
273
+ </div>
274
+
275
+ <div class="detail-panel" v-if="activeDetailTab === 'error' && query.status === 'failed'">
276
+ <div class="error-message">
277
+ <i class="bi bi-exclamation-triangle"></i>
278
+ <div class="error-content">
279
+ <h4>执行错误</h4>
280
+ <pre>{{ query.errorMessage }}</pre>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ <div class="detail-panel" v-if="activeDetailTab === 'plan'">
286
+ <div class="execution-plan">
287
+ <pre>{{ query.executionPlan || '暂无执行计划数据' }}</pre>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ </div>
295
+
296
+ <!-- 分页 -->
297
+ <div class="pagination" v-if="totalPages > 1">
298
+ <button @click="prevPage" :disabled="currentPage === 1">
299
+ <i class="bi bi-chevron-left"></i>
300
+ </button>
301
+ <span class="page-info">{{ currentPage }} / {{ totalPages }}</span>
302
+ <button @click="nextPage" :disabled="currentPage === totalPages">
303
+ <i class="bi bi-chevron-right"></i>
304
+ </button>
305
+ </div>
306
+ </div>
307
+
308
+ <!-- 确认清空对话框 -->
309
+ <div class="modal-overlay" v-if="showClearConfirm">
310
+ <div class="modal-content">
311
+ <div class="modal-header">
312
+ <h3>确认清空</h3>
313
+ <button class="modal-close" @click="showClearConfirm = false">
314
+ <i class="bi bi-x-lg"></i>
315
+ </button>
316
+ </div>
317
+ <div class="modal-body">
318
+ <p>确定要清空所有查询历史记录吗?此操作无法撤销。</p>
319
+ </div>
320
+ <div class="modal-footer">
321
+ <button class="btn-cancel" @click="showClearConfirm = false">取消</button>
322
+ <button class="btn-confirm" @click="clearHistory">确认清空</button>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </template>
328
+
329
+ <script lang="ts" setup>
330
+ import { ref, computed, onMounted } from 'vue';
331
+
332
+ // 响应式数据
333
+ const searchQuery = ref('');
334
+ const filterType = ref('all');
335
+ const activeFilter = ref('all');
336
+ const expandedQuery = ref<string | null>(null);
337
+ const activeDetailTab = ref('sql');
338
+ const currentPage = ref(1);
339
+ const pageSize = ref(20);
340
+ const showClearConfirm = ref(false);
341
+
342
+ // 模拟查询历史数据
343
+ const queryHistory = ref([
344
+ {
345
+ id: '1',
346
+ type: 'SELECT',
347
+ sql: 'SELECT * FROM users WHERE status = "active" AND created_at > "2024-01-01"',
348
+ timestamp: new Date(Date.now() - 5 * 60000),
349
+ executionTime: 245,
350
+ status: 'success',
351
+ connectionName: 'MySQL主库',
352
+ isFavorite: true,
353
+ affectedRows: 0,
354
+ returnedRows: 125,
355
+ resultPreview: {
356
+ columns: ['id', 'name', 'email', 'status'],
357
+ data: [
358
+ { id: 1, name: '张三', email: 'zhangsan@example.com', status: 'active' },
359
+ { id: 2, name: '李四', email: 'lisi@example.com', status: 'active' }
360
+ ]
361
+ }
362
+ },
363
+ {
364
+ id: '2',
365
+ type: 'UPDATE',
366
+ sql: 'UPDATE products SET price = price * 1.1 WHERE category_id = 5',
367
+ timestamp: new Date(Date.now() - 15 * 60000),
368
+ executionTime: 3200,
369
+ status: 'success',
370
+ connectionName: 'MySQL主库',
371
+ isFavorite: false,
372
+ affectedRows: 48,
373
+ returnedRows: 0
374
+ },
375
+ {
376
+ id: '3',
377
+ type: 'INSERT',
378
+ sql: 'INSERT INTO orders (user_id, product_id, quantity, total_amount) VALUES (1, 10, 2, 299.99)',
379
+ timestamp: new Date(Date.now() - 30 * 60000),
380
+ executionTime: 120,
381
+ status: 'success',
382
+ connectionName: 'MySQL主库',
383
+ isFavorite: false,
384
+ affectedRows: 1,
385
+ returnedRows: 0
386
+ },
387
+ {
388
+ id: '4',
389
+ type: 'SELECT',
390
+ sql: 'SELECT u.name, COUNT(o.id) as order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.created_at > "2024-01-01" GROUP BY u.id, u.name HAVING COUNT(o.id) > 5',
391
+ timestamp: new Date(Date.now() - 60 * 60000),
392
+ executionTime: 5600,
393
+ status: 'success',
394
+ connectionName: 'MySQL主库',
395
+ isFavorite: true,
396
+ affectedRows: 0,
397
+ returnedRows: 23
398
+ },
399
+ {
400
+ id: '5',
401
+ type: 'DELETE',
402
+ sql: 'DELETE FROM user_sessions WHERE last_activity < DATE_SUB(NOW(), INTERVAL 30 DAY)',
403
+ timestamp: new Date(Date.now() - 120 * 60000),
404
+ executionTime: 1850,
405
+ status: 'failed',
406
+ connectionName: 'MySQL主库',
407
+ isFavorite: false,
408
+ errorMessage: 'Error 1451: Cannot delete or update a parent row: a foreign key constraint fails'
409
+ }
410
+ ]);
411
+
412
+ // 计算属性
413
+ const totalQueries = computed(() => queryHistory.value.length);
414
+
415
+ const recentQueries = computed(() => {
416
+ const today = new Date();
417
+ today.setHours(0, 0, 0, 0);
418
+ return queryHistory.value.filter(q => q.timestamp >= today);
419
+ });
420
+
421
+ const favoriteQueries = computed(() =>
422
+ queryHistory.value.filter(q => q.isFavorite)
423
+ );
424
+
425
+ const averageExecutionTime = computed(() => {
426
+ const successfulQueries = queryHistory.value.filter(q => q.status === 'success');
427
+ if (successfulQueries.length === 0) return 0;
428
+ const total = successfulQueries.reduce((sum, q) => sum + q.executionTime, 0);
429
+ return total / successfulQueries.length;
430
+ });
431
+
432
+ const filteredQueries = computed(() => {
433
+ let filtered = queryHistory.value;
434
+
435
+ // 搜索过滤
436
+ if (searchQuery.value) {
437
+ const query = searchQuery.value.toLowerCase();
438
+ filtered = filtered.filter(q =>
439
+ q.sql.toLowerCase().includes(query) ||
440
+ q.connectionName.toLowerCase().includes(query)
441
+ );
442
+ }
443
+
444
+ // 类型过滤
445
+ if (filterType.value !== 'all') {
446
+ filtered = filtered.filter(q => q.type === filterType.value);
447
+ }
448
+
449
+ // 快速筛选
450
+ switch (activeFilter.value) {
451
+ case 'recent':
452
+ const today = new Date();
453
+ today.setHours(0, 0, 0, 0);
454
+ filtered = filtered.filter(q => q.timestamp >= today);
455
+ break;
456
+ case 'favorites':
457
+ filtered = filtered.filter(q => q.isFavorite);
458
+ break;
459
+ case 'slow':
460
+ filtered = filtered.filter(q => q.executionTime > 2000);
461
+ break;
462
+ case 'failed':
463
+ filtered = filtered.filter(q => q.status === 'failed');
464
+ break;
465
+ }
466
+
467
+ // 按时间倒序
468
+ return filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
469
+ });
470
+
471
+ const totalPages = computed(() =>
472
+ Math.ceil(filteredQueries.value.length / pageSize.value)
473
+ );
474
+
475
+ const paginatedQueries = computed(() => {
476
+ const start = (currentPage.value - 1) * pageSize.value;
477
+ const end = start + pageSize.value;
478
+ return filteredQueries.value.slice(start, end);
479
+ });
480
+
481
+ // 方法
482
+ function handleSearch() {
483
+ currentPage.value = 1;
484
+ }
485
+
486
+ function filterQueries() {
487
+ currentPage.value = 1;
488
+ }
489
+
490
+ function setActiveFilter(filter: string) {
491
+ activeFilter.value = filter;
492
+ currentPage.value = 1;
493
+ }
494
+
495
+ function toggleExpand(queryId: string) {
496
+ expandedQuery.value = expandedQuery.value === queryId ? null : queryId;
497
+ activeDetailTab.value = 'sql';
498
+ }
499
+
500
+ function toggleFavorite(query: any) {
501
+ query.isFavorite = !query.isFavorite;
502
+ // 这里可以调用API更新收藏状态
503
+ }
504
+
505
+ function executeQuery(query: any) {
506
+ console.log('执行查询:', query);
507
+ // 这里应该跳转到SQL编辑器并加载查询
508
+ }
509
+
510
+ function copyQuery(query: any) {
511
+ copySql(query.sql);
512
+ }
513
+
514
+ function copySql(sql: string) {
515
+ navigator.clipboard.writeText(sql).then(() => {
516
+ // 这里可以显示复制成功提示
517
+ console.log('SQL已复制到剪贴板');
518
+ });
519
+ }
520
+
521
+ function deleteQuery(query: any) {
522
+ const index = queryHistory.value.findIndex(q => q.id === query.id);
523
+ if (index > -1) {
524
+ queryHistory.value.splice(index, 1);
525
+ // 这里可以调用API删除记录
526
+ }
527
+ }
528
+
529
+ function truncateSql(sql: string, maxLength = 80): string {
530
+ if (sql.length <= maxLength) return sql;
531
+ return sql.substring(0, maxLength) + '...';
532
+ }
533
+
534
+ function formatTime(timestamp: Date): string {
535
+ const now = new Date();
536
+ const diff = now.getTime() - timestamp.getTime();
537
+ const minutes = Math.floor(diff / 60000);
538
+ const hours = Math.floor(diff / 3600000);
539
+ const days = Math.floor(diff / 86400000);
540
+
541
+ if (minutes < 1) return '刚刚';
542
+ if (minutes < 60) return `${minutes}分钟前`;
543
+ if (hours < 24) return `${hours}小时前`;
544
+ if (days < 7) return `${days}天前`;
545
+
546
+ return timestamp.toLocaleDateString('zh-CN');
547
+ }
548
+
549
+ function getExecutionTimeClass(time: number): string {
550
+ if (time < 100) return 'fast';
551
+ if (time < 1000) return 'normal';
552
+ if (time < 2000) return 'slow';
553
+ return 'very-slow';
554
+ }
555
+
556
+ function confirmClearHistory() {
557
+ showClearConfirm.value = true;
558
+ }
559
+
560
+ function clearHistory() {
561
+ queryHistory.value = [];
562
+ showClearConfirm.value = false;
563
+ currentPage.value = 1;
564
+ }
565
+
566
+ function exportHistory() {
567
+ const data = filteredQueries.value.map(q => ({
568
+ 时间: q.timestamp.toLocaleString('zh-CN'),
569
+ 类型: q.type,
570
+ 连接: q.connectionName,
571
+ 执行时间: q.executionTime + 'ms',
572
+ 状态: q.status,
573
+ SQL: q.sql
574
+ }));
575
+
576
+ const csv = [
577
+ Object.keys(data[0]).join(','),
578
+ ...data.map(row => Object.values(row).map(v => `"${v}"`).join(','))
579
+ ].join('\n');
580
+
581
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
582
+ const link = document.createElement('a');
583
+ link.href = URL.createObjectURL(blob);
584
+ link.download = `query_history_${new Date().getTime()}.csv`;
585
+ link.click();
586
+ }
587
+
588
+ function nextPage() {
589
+ if (currentPage.value < totalPages.value) {
590
+ currentPage.value++;
591
+ }
592
+ }
593
+
594
+ function prevPage() {
595
+ if (currentPage.value > 1) {
596
+ currentPage.value--;
597
+ }
598
+ }
599
+
600
+ // 生命周期
601
+ onMounted(() => {
602
+ // 加载查询历史数据
603
+ });
604
+ </script>
605
+
606
+ <style scoped>
607
+ .query-history {
608
+ height: 100%;
609
+ display: flex;
610
+ flex-direction: column;
611
+ background: white;
612
+ border-radius: 12px;
613
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
614
+ overflow: hidden;
615
+ }
616
+
617
+ .history-header {
618
+ display: flex;
619
+ justify-content: space-between;
620
+ align-items: center;
621
+ padding: 1rem 1.5rem;
622
+ background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
623
+ border-bottom: 1px solid #e2e8f0;
624
+ }
625
+
626
+ .header-title {
627
+ display: flex;
628
+ align-items: center;
629
+ gap: 0.5rem;
630
+ font-weight: 600;
631
+ color: #1e293b;
632
+ font-size: 1.1rem;
633
+ }
634
+
635
+ .header-controls {
636
+ display: flex;
637
+ align-items: center;
638
+ gap: 1rem;
639
+ }
640
+
641
+ .search-box {
642
+ position: relative;
643
+ display: flex;
644
+ align-items: center;
645
+ }
646
+
647
+ .search-box i {
648
+ position: absolute;
649
+ left: 0.75rem;
650
+ color: #6b7280;
651
+ font-size: 0.875rem;
652
+ }
653
+
654
+ .search-box input {
655
+ padding: 0.5rem 0.75rem 0.5rem 2.25rem;
656
+ border: 1px solid #d1d5db;
657
+ border-radius: 8px;
658
+ font-size: 0.875rem;
659
+ width: 250px;
660
+ }
661
+
662
+ .search-box input:focus {
663
+ outline: none;
664
+ border-color: #667eea;
665
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
666
+ }
667
+
668
+ .filter-select {
669
+ padding: 0.5rem 0.75rem;
670
+ border: 1px solid #d1d5db;
671
+ border-radius: 8px;
672
+ background: white;
673
+ font-size: 0.875rem;
674
+ }
675
+
676
+ .btn-clear-history {
677
+ display: flex;
678
+ align-items: center;
679
+ gap: 0.25rem;
680
+ padding: 0.5rem 1rem;
681
+ border: 1px solid #ef4444;
682
+ border-radius: 8px;
683
+ background: white;
684
+ color: #ef4444;
685
+ font-size: 0.875rem;
686
+ cursor: pointer;
687
+ transition: all 0.2s ease;
688
+ }
689
+
690
+ .btn-clear-history:hover {
691
+ background: #ef4444;
692
+ color: white;
693
+ }
694
+
695
+ .history-content {
696
+ flex: 1;
697
+ overflow-y: auto;
698
+ padding: 1.5rem;
699
+ }
700
+
701
+ /* 统计卡片 */
702
+ .stats-section {
703
+ display: grid;
704
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
705
+ gap: 1rem;
706
+ margin-bottom: 2rem;
707
+ }
708
+
709
+ .stat-card {
710
+ display: flex;
711
+ align-items: center;
712
+ gap: 1rem;
713
+ padding: 1.25rem;
714
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
715
+ border: 1px solid #e2e8f0;
716
+ border-radius: 12px;
717
+ transition: all 0.2s ease;
718
+ }
719
+
720
+ .stat-card:hover {
721
+ transform: translateY(-2px);
722
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
723
+ }
724
+
725
+ .stat-icon {
726
+ width: 48px;
727
+ height: 48px;
728
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
729
+ border-radius: 12px;
730
+ display: flex;
731
+ align-items: center;
732
+ justify-content: center;
733
+ color: white;
734
+ font-size: 1.25rem;
735
+ }
736
+
737
+ .stat-info {
738
+ flex: 1;
739
+ }
740
+
741
+ .stat-number {
742
+ font-size: 1.5rem;
743
+ font-weight: 700;
744
+ color: #1e293b;
745
+ margin-bottom: 0.25rem;
746
+ }
747
+
748
+ .stat-label {
749
+ font-size: 0.875rem;
750
+ color: #64748b;
751
+ }
752
+
753
+ /* 快速筛选 */
754
+ .quick-filters {
755
+ display: flex;
756
+ gap: 0.5rem;
757
+ margin-bottom: 2rem;
758
+ flex-wrap: wrap;
759
+ }
760
+
761
+ .filter-tag {
762
+ display: flex;
763
+ align-items: center;
764
+ gap: 0.5rem;
765
+ padding: 0.5rem 1rem;
766
+ border: 1px solid #d1d5db;
767
+ border-radius: 20px;
768
+ background: white;
769
+ color: #6b7280;
770
+ font-size: 0.875rem;
771
+ cursor: pointer;
772
+ transition: all 0.2s ease;
773
+ }
774
+
775
+ .filter-tag:hover {
776
+ background: #f3f4f6;
777
+ border-color: #9ca3af;
778
+ }
779
+
780
+ .filter-tag.active {
781
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
782
+ color: white;
783
+ border-color: transparent;
784
+ }
785
+
786
+ /* 查询列表 */
787
+ .queries-list {
788
+ flex: 1;
789
+ }
790
+
791
+ .list-header {
792
+ display: flex;
793
+ justify-content: space-between;
794
+ align-items: center;
795
+ margin-bottom: 1rem;
796
+ }
797
+
798
+ .list-title {
799
+ font-size: 1rem;
800
+ font-weight: 600;
801
+ color: #1e293b;
802
+ }
803
+
804
+ .list-actions {
805
+ display: flex;
806
+ gap: 0.5rem;
807
+ }
808
+
809
+ .btn-export {
810
+ display: flex;
811
+ align-items: center;
812
+ gap: 0.25rem;
813
+ padding: 0.5rem 1rem;
814
+ border: 1px solid #667eea;
815
+ border-radius: 8px;
816
+ background: white;
817
+ color: #667eea;
818
+ font-size: 0.875rem;
819
+ cursor: pointer;
820
+ transition: all 0.2s ease;
821
+ }
822
+
823
+ .btn-export:hover {
824
+ background: #667eea;
825
+ color: white;
826
+ }
827
+
828
+ .query-items {
829
+ display: flex;
830
+ flex-direction: column;
831
+ gap: 0.75rem;
832
+ }
833
+
834
+ .query-item {
835
+ border: 1px solid #e5e7eb;
836
+ border-radius: 12px;
837
+ overflow: hidden;
838
+ transition: all 0.2s ease;
839
+ }
840
+
841
+ .query-item:hover {
842
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
843
+ }
844
+
845
+ .query-item.failed {
846
+ border-left: 4px solid #ef4444;
847
+ }
848
+
849
+ .query-item.slow {
850
+ border-left: 4px solid #f59e0b;
851
+ }
852
+
853
+ .query-item.expanded {
854
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
855
+ }
856
+
857
+ .query-header {
858
+ display: flex;
859
+ justify-content: space-between;
860
+ align-items: center;
861
+ padding: 1rem 1.25rem;
862
+ cursor: pointer;
863
+ transition: background-color 0.2s ease;
864
+ }
865
+
866
+ .query-header:hover {
867
+ background: #f8fafc;
868
+ }
869
+
870
+ .query-info {
871
+ flex: 1;
872
+ display: flex;
873
+ align-items: center;
874
+ gap: 1rem;
875
+ min-width: 0;
876
+ }
877
+
878
+ .query-type-badge {
879
+ padding: 0.25rem 0.75rem;
880
+ border-radius: 20px;
881
+ font-size: 0.75rem;
882
+ font-weight: 600;
883
+ color: white;
884
+ white-space: nowrap;
885
+ }
886
+
887
+ .query-type-badge.select { background: #3b82f6; }
888
+ .query-type-badge.insert { background: #10b981; }
889
+ .query-type-badge.update { background: #f59e0b; }
890
+ .query-type-badge.delete { background: #ef4444; }
891
+ .query-type-badge.create { background: #8b5cf6; }
892
+ .query-type-badge.alter { background: #06b6d4; }
893
+ .query-type-badge.drop { background: #dc2626; }
894
+
895
+ .query-summary {
896
+ flex: 1;
897
+ min-width: 0;
898
+ }
899
+
900
+ .query-text {
901
+ font-family: 'Consolas', 'Monaco', monospace;
902
+ font-size: 0.875rem;
903
+ color: #374151;
904
+ display: block;
905
+ }
906
+
907
+ .query-meta {
908
+ display: flex;
909
+ align-items: center;
910
+ gap: 1rem;
911
+ font-size: 0.75rem;
912
+ color: #6b7280;
913
+ }
914
+
915
+ .execution-time {
916
+ font-weight: 600;
917
+ padding: 0.125rem 0.5rem;
918
+ border-radius: 4px;
919
+ }
920
+
921
+ .execution-time.fast { color: #10b981; background: #d1fae5; }
922
+ .execution-time.normal { color: #3b82f6; background: #dbeafe; }
923
+ .execution-time.slow { color: #f59e0b; background: #fef3c7; }
924
+ .execution-time.very-slow { color: #ef4444; background: #fee2e2; }
925
+
926
+ .query-actions {
927
+ display: flex;
928
+ align-items: center;
929
+ gap: 0.25rem;
930
+ }
931
+
932
+ .query-actions button {
933
+ width: 32px;
934
+ height: 32px;
935
+ border: 1px solid #e5e7eb;
936
+ background: white;
937
+ border-radius: 6px;
938
+ display: flex;
939
+ align-items: center;
940
+ justify-content: center;
941
+ cursor: pointer;
942
+ transition: all 0.2s ease;
943
+ }
944
+
945
+ .query-actions button:hover {
946
+ background: #f3f4f6;
947
+ border-color: #d1d5db;
948
+ }
949
+
950
+ .btn-favorite.active {
951
+ background: #fbbf24;
952
+ border-color: #fbbf24;
953
+ color: white;
954
+ }
955
+
956
+ .btn-execute:hover {
957
+ background: #10b981;
958
+ border-color: #10b981;
959
+ color: white;
960
+ }
961
+
962
+ .btn-copy:hover {
963
+ background: #3b82f6;
964
+ border-color: #3b82f6;
965
+ color: white;
966
+ }
967
+
968
+ .btn-delete:hover {
969
+ background: #ef4444;
970
+ border-color: #ef4444;
971
+ color: white;
972
+ }
973
+
974
+ .query-details {
975
+ border-top: 1px solid #e5e7eb;
976
+ background: #fafbfc;
977
+ }
978
+
979
+ .details-tabs {
980
+ display: flex;
981
+ border-bottom: 1px solid #e5e7eb;
982
+ }
983
+
984
+ .tab-btn {
985
+ padding: 0.75rem 1.5rem;
986
+ background: none;
987
+ border: none;
988
+ color: #6b7280;
989
+ font-size: 0.875rem;
990
+ cursor: pointer;
991
+ transition: all 0.2s ease;
992
+ border-bottom: 2px solid transparent;
993
+ }
994
+
995
+ .tab-btn:hover {
996
+ color: #374151;
997
+ }
998
+
999
+ .tab-btn.active {
1000
+ color: #667eea;
1001
+ border-bottom-color: #667eea;
1002
+ background: white;
1003
+ }
1004
+
1005
+ .details-content {
1006
+ padding: 1.25rem;
1007
+ }
1008
+
1009
+ .detail-panel {
1010
+ min-height: 100px;
1011
+ }
1012
+
1013
+ .sql-editor {
1014
+ position: relative;
1015
+ background: #1e293b;
1016
+ border-radius: 8px;
1017
+ padding: 1rem;
1018
+ overflow-x: auto;
1019
+ }
1020
+
1021
+ .sql-editor pre {
1022
+ margin: 0;
1023
+ color: #e2e8f0;
1024
+ font-family: 'Consolas', 'Monaco', monospace;
1025
+ font-size: 0.875rem;
1026
+ line-height: 1.5;
1027
+ }
1028
+
1029
+ .btn-copy-sql {
1030
+ position: absolute;
1031
+ top: 0.5rem;
1032
+ right: 0.5rem;
1033
+ padding: 0.25rem 0.5rem;
1034
+ background: rgba(255, 255, 255, 0.1);
1035
+ border: 1px solid rgba(255, 255, 255, 0.2);
1036
+ border-radius: 4px;
1037
+ color: white;
1038
+ font-size: 0.75rem;
1039
+ cursor: pointer;
1040
+ transition: all 0.2s ease;
1041
+ }
1042
+
1043
+ .btn-copy-sql:hover {
1044
+ background: rgba(255, 255, 255, 0.2);
1045
+ }
1046
+
1047
+ .result-info {
1048
+ display: grid;
1049
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1050
+ gap: 1rem;
1051
+ margin-bottom: 1.5rem;
1052
+ }
1053
+
1054
+ .result-stat {
1055
+ display: flex;
1056
+ flex-direction: column;
1057
+ gap: 0.25rem;
1058
+ }
1059
+
1060
+ .stat-label {
1061
+ font-size: 0.875rem;
1062
+ color: #6b7280;
1063
+ }
1064
+
1065
+ .stat-value {
1066
+ font-size: 1.125rem;
1067
+ font-weight: 600;
1068
+ color: #1e293b;
1069
+ }
1070
+
1071
+ .result-preview {
1072
+ border: 1px solid #e5e7eb;
1073
+ border-radius: 8px;
1074
+ overflow: hidden;
1075
+ }
1076
+
1077
+ .preview-table {
1078
+ width: 100%;
1079
+ border-collapse: collapse;
1080
+ font-size: 0.875rem;
1081
+ }
1082
+
1083
+ .preview-table th {
1084
+ background: #f8fafc;
1085
+ padding: 0.75rem;
1086
+ text-align: left;
1087
+ font-weight: 600;
1088
+ color: #374151;
1089
+ border-bottom: 1px solid #e5e7eb;
1090
+ }
1091
+
1092
+ .preview-table td {
1093
+ padding: 0.75rem;
1094
+ border-bottom: 1px solid #f3f4f6;
1095
+ }
1096
+
1097
+ .error-message {
1098
+ display: flex;
1099
+ align-items: flex-start;
1100
+ gap: 1rem;
1101
+ padding: 1rem;
1102
+ background: #fef2f2;
1103
+ border: 1px solid #fecaca;
1104
+ border-radius: 8px;
1105
+ color: #991b1b;
1106
+ }
1107
+
1108
+ .error-message i {
1109
+ font-size: 1.25rem;
1110
+ color: #ef4444;
1111
+ margin-top: 0.125rem;
1112
+ }
1113
+
1114
+ .error-content h4 {
1115
+ margin: 0 0 0.5rem 0;
1116
+ font-size: 1rem;
1117
+ }
1118
+
1119
+ .error-content pre {
1120
+ margin: 0;
1121
+ background: #fee2e2;
1122
+ padding: 0.75rem;
1123
+ border-radius: 4px;
1124
+ font-family: 'Consolas', 'Monaco', monospace;
1125
+ font-size: 0.875rem;
1126
+ }
1127
+
1128
+ .execution-plan {
1129
+ background: #f8fafc;
1130
+ border: 1px solid #e5e7eb;
1131
+ border-radius: 8px;
1132
+ padding: 1rem;
1133
+ }
1134
+
1135
+ .execution-plan pre {
1136
+ margin: 0;
1137
+ font-family: 'Consolas', 'Monaco', monospace;
1138
+ font-size: 0.875rem;
1139
+ color: #374151;
1140
+ }
1141
+
1142
+ /* 分页 */
1143
+ .pagination {
1144
+ display: flex;
1145
+ justify-content: center;
1146
+ align-items: center;
1147
+ gap: 0.5rem;
1148
+ margin-top: 2rem;
1149
+ padding: 1rem;
1150
+ background: #f8fafc;
1151
+ border-radius: 8px;
1152
+ }
1153
+
1154
+ .pagination button {
1155
+ padding: 0.5rem 0.75rem;
1156
+ border: 1px solid #d1d5db;
1157
+ border-radius: 6px;
1158
+ background: white;
1159
+ cursor: pointer;
1160
+ transition: all 0.2s ease;
1161
+ }
1162
+
1163
+ .pagination button:hover:not(:disabled) {
1164
+ background: #667eea;
1165
+ color: white;
1166
+ border-color: #667eea;
1167
+ }
1168
+
1169
+ .pagination button:disabled {
1170
+ opacity: 0.5;
1171
+ cursor: not-allowed;
1172
+ }
1173
+
1174
+ .page-info {
1175
+ padding: 0.5rem 1rem;
1176
+ font-size: 0.875rem;
1177
+ color: #6b7280;
1178
+ }
1179
+
1180
+ /* 模态框 */
1181
+ .modal-overlay {
1182
+ position: fixed;
1183
+ top: 0;
1184
+ left: 0;
1185
+ right: 0;
1186
+ bottom: 0;
1187
+ background: rgba(0, 0, 0, 0.5);
1188
+ display: flex;
1189
+ justify-content: center;
1190
+ align-items: center;
1191
+ z-index: 1000;
1192
+ }
1193
+
1194
+ .modal-content {
1195
+ background: white;
1196
+ border-radius: 12px;
1197
+ width: 90%;
1198
+ max-width: 400px;
1199
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
1200
+ }
1201
+
1202
+ .modal-header {
1203
+ display: flex;
1204
+ justify-content: space-between;
1205
+ align-items: center;
1206
+ padding: 1.5rem;
1207
+ border-bottom: 1px solid #e5e7eb;
1208
+ }
1209
+
1210
+ .modal-header h3 {
1211
+ margin: 0;
1212
+ font-size: 1.25rem;
1213
+ font-weight: 600;
1214
+ color: #1e293b;
1215
+ }
1216
+
1217
+ .modal-close {
1218
+ background: none;
1219
+ border: none;
1220
+ font-size: 1.25rem;
1221
+ cursor: pointer;
1222
+ color: #6b7280;
1223
+ padding: 0.25rem;
1224
+ }
1225
+
1226
+ .modal-close:hover {
1227
+ color: #374151;
1228
+ }
1229
+
1230
+ .modal-body {
1231
+ padding: 1.5rem;
1232
+ }
1233
+
1234
+ .modal-body p {
1235
+ margin: 0;
1236
+ color: #374151;
1237
+ line-height: 1.5;
1238
+ }
1239
+
1240
+ .modal-footer {
1241
+ display: flex;
1242
+ justify-content: flex-end;
1243
+ gap: 0.5rem;
1244
+ padding: 1.5rem;
1245
+ border-top: 1px solid #e5e7eb;
1246
+ }
1247
+
1248
+ .btn-cancel,
1249
+ .btn-confirm {
1250
+ padding: 0.75rem 1.5rem;
1251
+ border-radius: 8px;
1252
+ font-weight: 500;
1253
+ cursor: pointer;
1254
+ transition: all 0.2s ease;
1255
+ }
1256
+
1257
+ .btn-cancel {
1258
+ background: white;
1259
+ border: 1px solid #d1d5db;
1260
+ color: #374151;
1261
+ }
1262
+
1263
+ .btn-cancel:hover {
1264
+ background: #f3f4f6;
1265
+ }
1266
+
1267
+ .btn-confirm {
1268
+ background: #ef4444;
1269
+ border: none;
1270
+ color: white;
1271
+ }
1272
+
1273
+ .btn-confirm:hover {
1274
+ background: #dc2626;
1275
+ }
1276
+
1277
+ /* 响应式设计 */
1278
+ @media (max-width: 768px) {
1279
+ .history-header {
1280
+ flex-direction: column;
1281
+ gap: 1rem;
1282
+ align-items: stretch;
1283
+ }
1284
+
1285
+ .header-controls {
1286
+ flex-wrap: wrap;
1287
+ }
1288
+
1289
+ .search-box input {
1290
+ width: 100%;
1291
+ }
1292
+
1293
+ .stats-section {
1294
+ grid-template-columns: repeat(2, 1fr);
1295
+ }
1296
+
1297
+ .quick-filters {
1298
+ justify-content: center;
1299
+ }
1300
+
1301
+ .query-header {
1302
+ flex-direction: column;
1303
+ gap: 1rem;
1304
+ align-items: stretch;
1305
+ }
1306
+
1307
+ .query-info {
1308
+ flex-direction: column;
1309
+ align-items: stretch;
1310
+ gap: 0.75rem;
1311
+ }
1312
+
1313
+ .query-meta {
1314
+ justify-content: space-between;
1315
+ }
1316
+
1317
+ .query-actions {
1318
+ justify-content: center;
1319
+ }
1320
+
1321
+ .details-tabs {
1322
+ overflow-x: auto;
1323
+ }
1324
+
1325
+ .tab-btn {
1326
+ flex-shrink: 0;
1327
+ }
1328
+ }
1329
+
1330
+ @media (max-width: 480px) {
1331
+ .history-content {
1332
+ padding: 1rem;
1333
+ }
1334
+
1335
+ .stats-section {
1336
+ grid-template-columns: 1fr;
1337
+ }
1338
+
1339
+ .list-header {
1340
+ flex-direction: column;
1341
+ gap: 1rem;
1342
+ align-items: stretch;
1343
+ }
1344
+
1345
+ .result-info {
1346
+ grid-template-columns: 1fr;
1347
+ }
1348
+ }
1349
1349
  </style>