fdb2 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.dockerignore +21 -0
  2. package/.editorconfig +11 -0
  3. package/.eslintrc.cjs +14 -0
  4. package/.eslintrc.json +7 -0
  5. package/.prettierrc.js +3 -0
  6. package/.tpl.env +22 -0
  7. package/README.md +260 -0
  8. package/bin/build.sh +28 -0
  9. package/bin/deploy.sh +8 -0
  10. package/bin/dev.sh +10 -0
  11. package/bin/docker/.env +4 -0
  12. package/bin/docker/dev-docker-compose.yml +43 -0
  13. package/bin/docker/dev.Dockerfile +24 -0
  14. package/bin/docker/prod-docker-compose.yml +17 -0
  15. package/bin/docker/prod.Dockerfile +29 -0
  16. package/bin/fdb2.js +142 -0
  17. package/data/connections.demo.json +32 -0
  18. package/env.d.ts +1 -0
  19. package/nw-build.js +120 -0
  20. package/nw-dev.js +65 -0
  21. package/package.json +114 -0
  22. package/public/favicon.ico +0 -0
  23. package/public/index.html +9 -0
  24. package/public/modules/header.tpl +14 -0
  25. package/public/modules/initial_state.tpl +55 -0
  26. package/server/index.ts +677 -0
  27. package/server/model/connection.entity.ts +66 -0
  28. package/server/model/database.entity.ts +246 -0
  29. package/server/service/connection.service.ts +334 -0
  30. package/server/service/database/base.service.ts +363 -0
  31. package/server/service/database/database.service.ts +510 -0
  32. package/server/service/database/index.ts +7 -0
  33. package/server/service/database/mssql.service.ts +723 -0
  34. package/server/service/database/mysql.service.ts +761 -0
  35. package/server/service/database/oracle.service.ts +839 -0
  36. package/server/service/database/postgres.service.ts +744 -0
  37. package/server/service/database/sqlite.service.ts +559 -0
  38. package/server/service/session.service.ts +158 -0
  39. package/server.js +128 -0
  40. package/src/adapter/ajax.ts +135 -0
  41. package/src/assets/base.css +1 -0
  42. package/src/assets/database.css +950 -0
  43. package/src/assets/images/collapse.png +0 -0
  44. package/src/assets/images/no-login.png +0 -0
  45. package/src/assets/images/svg/illustrations/illustration-1.svg +1 -0
  46. package/src/assets/images/svg/illustrations/illustration-2.svg +2 -0
  47. package/src/assets/images/svg/illustrations/illustration-3.svg +50 -0
  48. package/src/assets/images/svg/illustrations/illustration-4.svg +1 -0
  49. package/src/assets/images/svg/illustrations/illustration-5.svg +73 -0
  50. package/src/assets/images/svg/illustrations/illustration-6.svg +89 -0
  51. package/src/assets/images/svg/illustrations/illustration-7.svg +39 -0
  52. package/src/assets/images/svg/illustrations/illustration-8.svg +1 -0
  53. package/src/assets/images/svg/separators/curve-2.svg +3 -0
  54. package/src/assets/images/svg/separators/curve.svg +3 -0
  55. package/src/assets/images/svg/separators/line.svg +3 -0
  56. package/src/assets/images/theme/light/screen-1-1000x800.jpg +0 -0
  57. package/src/assets/images/theme/light/screen-2-1000x800.jpg +0 -0
  58. package/src/assets/login/bg.jpg +0 -0
  59. package/src/assets/login/bg.png +0 -0
  60. package/src/assets/login/left.jpg +0 -0
  61. package/src/assets/logo.svg +73 -0
  62. package/src/assets/logo.webp +0 -0
  63. package/src/assets/main.css +1 -0
  64. package/src/base/config.ts +20 -0
  65. package/src/base/detect.ts +134 -0
  66. package/src/base/entity.ts +92 -0
  67. package/src/base/eventBus.ts +37 -0
  68. package/src/base//345/237/272/347/241/200/345/261/202.md +7 -0
  69. package/src/components/connection-editor/index.vue +590 -0
  70. package/src/components/dataGrid/index.vue +105 -0
  71. package/src/components/dataGrid/pagination.vue +106 -0
  72. package/src/components/loading/index.vue +43 -0
  73. package/src/components/modal/index.ts +181 -0
  74. package/src/components/modal/index.vue +560 -0
  75. package/src/components/toast/index.ts +44 -0
  76. package/src/components/toast/toast.vue +58 -0
  77. package/src/components/user/name.vue +104 -0
  78. package/src/components/user/selector.vue +416 -0
  79. package/src/domain/SysConfig.ts +74 -0
  80. package/src/platform/App.vue +8 -0
  81. package/src/platform/database/components/connection-detail.vue +1154 -0
  82. package/src/platform/database/components/data-editor.vue +478 -0
  83. package/src/platform/database/components/data-import-export.vue +1602 -0
  84. package/src/platform/database/components/database-detail.vue +1173 -0
  85. package/src/platform/database/components/database-monitor.vue +1086 -0
  86. package/src/platform/database/components/db-tools.vue +577 -0
  87. package/src/platform/database/components/query-history.vue +1349 -0
  88. package/src/platform/database/components/sql-executor.vue +738 -0
  89. package/src/platform/database/components/sql-query-editor.vue +1046 -0
  90. package/src/platform/database/components/table-detail.vue +1376 -0
  91. package/src/platform/database/components/table-editor.vue +690 -0
  92. package/src/platform/database/explorer.vue +1840 -0
  93. package/src/platform/database/index.vue +1193 -0
  94. package/src/platform/database/layout.vue +367 -0
  95. package/src/platform/database/router.ts +37 -0
  96. package/src/platform/database/styles/common.scss +602 -0
  97. package/src/platform/database/types/common.ts +445 -0
  98. package/src/platform/database/utils/export.ts +232 -0
  99. package/src/platform/database/utils/helpers.ts +437 -0
  100. package/src/platform/index.ts +33 -0
  101. package/src/platform/router.ts +41 -0
  102. package/src/service/base.ts +128 -0
  103. package/src/service/database.ts +500 -0
  104. package/src/service/login.ts +121 -0
  105. package/src/shims-vue.d.ts +7 -0
  106. package/src/stores/connection.ts +266 -0
  107. package/src/stores/session.ts +87 -0
  108. package/src/typings/database-types.ts +413 -0
  109. package/src/typings/database.ts +364 -0
  110. package/src/typings/global.d.ts +58 -0
  111. package/src/typings/pinia.d.ts +8 -0
  112. package/src/utils/clipboard.ts +30 -0
  113. package/src/utils/database-types.ts +243 -0
  114. package/src/utils/modal.ts +124 -0
  115. package/src/utils/request.ts +55 -0
  116. package/src/utils/sleep.ts +4 -0
  117. package/src/utils/toast.ts +73 -0
  118. package/src/utils/util.ts +171 -0
  119. package/src/utils/xlsx.ts +228 -0
  120. package/tsconfig.json +33 -0
  121. package/tsconfig.server.json +19 -0
  122. package/view/index.html +9 -0
  123. package/view/modules/header.tpl +14 -0
  124. package/view/modules/initial_state.tpl +20 -0
  125. package/vite.config.ts +384 -0
@@ -0,0 +1,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
+ }
1349
+ </style>