fdb2 1.0.11 → 1.0.13
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.
- package/bin/fdb2.js +43 -33
- package/dist/public/explorer.css +185 -103
- package/dist/public/explorer.js +855 -896
- package/dist/public/index.css +30 -2
- package/dist/public/index.js +61 -16
- package/dist/public/vue.js +18 -25
- package/dist/server/service/database/sqlite.service.d.ts +0 -1
- package/dist/server/service/database/sqlite.service.d.ts.map +1 -1
- package/dist/server/service/database/sqlite.service.js +0 -1
- package/dist/server/service/database/sqlite.service.js.map +1 -1
- package/dist/server/service/database/sqlite.service.ts +0 -1
- package/package.json +22 -20
- package/packages/vscode/package.json +3 -1
- package/packages/vscode/src/database-services/sqlite.service.ts +0 -1
- package/server/service/database/sqlite.service.ts +0 -1
- package/src/components/dataGrid/index.vue +62 -4
- package/src/platform/database/components/database-detail.vue +31 -6
- package/src/platform/database/components/table-data-grid.vue +273 -0
- package/src/platform/database/components/table-detail.vue +64 -267
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
</div>
|
|
77
77
|
</div>
|
|
78
78
|
<div class="toolbar-right">
|
|
79
|
-
<button class="btn btn-outline-warning btn-sm" @click="truncateTable" v-if="
|
|
79
|
+
<button class="btn btn-outline-warning btn-sm" @click="truncateTable" v-if="table?.rowCount">
|
|
80
80
|
<i class="bi bi-trash"></i> 清空表
|
|
81
81
|
</button>
|
|
82
82
|
<button class="btn btn-outline-danger btn-sm" @click="dropTable">
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
@click="activeTab = 'data'"
|
|
96
96
|
>
|
|
97
97
|
<i class="bi bi-grid"></i> 数据
|
|
98
|
-
<span class="badge bg-secondary ms-2" v-if="
|
|
98
|
+
<span class="badge bg-secondary ms-2" v-if="table?.rowCount">{{ formatNumber(table?.rowCount) }}</span>
|
|
99
99
|
</button>
|
|
100
100
|
</li>
|
|
101
101
|
<li class="nav-item">
|
|
@@ -139,148 +139,15 @@
|
|
|
139
139
|
<div class="tab-content">
|
|
140
140
|
<!-- 数据标签页 -->
|
|
141
141
|
<div v-show="activeTab === 'data'" class="tab-panel">
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<span class="column-key" v-if="column.isPrimary">
|
|
152
|
-
<i class="bi bi-key-fill"></i>
|
|
153
|
-
</span>
|
|
154
|
-
</div>
|
|
155
|
-
</th>
|
|
156
|
-
<th width="100">操作</th>
|
|
157
|
-
</tr>
|
|
158
|
-
</thead>
|
|
159
|
-
<tbody>
|
|
160
|
-
<tr v-for="(row, index) in paginatedData" :key="index">
|
|
161
|
-
<td v-for="column in safeTableColumns" :key="column.name">
|
|
162
|
-
<div class="cell-value">
|
|
163
|
-
{{ formatCellValue(row[column.name]) }}
|
|
164
|
-
</div>
|
|
165
|
-
</td>
|
|
166
|
-
<td>
|
|
167
|
-
<div class="btn-group btn-group-sm">
|
|
168
|
-
<button class="btn btn-outline-primary btn-sm" @click="editRow(row)">
|
|
169
|
-
<i class="bi bi-pencil"></i>
|
|
170
|
-
</button>
|
|
171
|
-
<button class="btn btn-outline-danger btn-sm" @click="deleteRow(row)">
|
|
172
|
-
<i class="bi bi-trash"></i>
|
|
173
|
-
</button>
|
|
174
|
-
</div>
|
|
175
|
-
</td>
|
|
176
|
-
</tr>
|
|
177
|
-
</tbody>
|
|
178
|
-
</table>
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
<!-- 加载状态 -->
|
|
182
|
-
<div v-if="loading" class="loading-state">
|
|
183
|
-
<div class="spinner-border text-primary" role="status">
|
|
184
|
-
<span class="visually-hidden">加载中...</span>
|
|
185
|
-
</div>
|
|
186
|
-
<p>正在加载数据...</p>
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
<!-- 空状态 -->
|
|
190
|
-
<div v-if="!loading && paginatedData.length === 0" class="empty-state">
|
|
191
|
-
<i class="bi bi-inbox"></i>
|
|
192
|
-
<p v-if="searchQuery">没有找到匹配的数据</p>
|
|
193
|
-
<p v-else>表中暂无数据</p>
|
|
194
|
-
<button class="btn btn-success" @click="()=>insertData()">
|
|
195
|
-
<i class="bi bi-plus"></i> 插入第一条数据
|
|
196
|
-
</button>
|
|
197
|
-
</div>
|
|
198
|
-
|
|
199
|
-
<!-- 分页 -->
|
|
200
|
-
<nav v-if="!loading && totalPages > 0" class="pagination-nav">
|
|
201
|
-
<div class="pagination-container">
|
|
202
|
-
<div class="pagination-info">
|
|
203
|
-
共 {{ formatNumber(total) }} 条记录,第 {{ formatNumber(currentPage) }} 页/共 {{ formatNumber(totalPages) }} 页
|
|
204
|
-
</div>
|
|
205
|
-
<ul class="pagination pagination-sm">
|
|
206
|
-
<li class="page-item" :class="{ disabled: currentPage === 1 }">
|
|
207
|
-
<a class="page-link" href="#" @click.prevent="goToPage(1)" title="首页">
|
|
208
|
-
<i class="bi bi-chevron-double-left"></i>
|
|
209
|
-
</a>
|
|
210
|
-
</li>
|
|
211
|
-
<li class="page-item" :class="{ disabled: currentPage === 1 }">
|
|
212
|
-
<a class="page-link" href="#" @click.prevent="goToPage(currentPage - 1)" title="上一页">
|
|
213
|
-
<i class="bi bi-chevron-left"></i>
|
|
214
|
-
</a>
|
|
215
|
-
</li>
|
|
216
|
-
|
|
217
|
-
<!-- 第一页和省略号 -->
|
|
218
|
-
<li v-if="currentPage > 4" class="page-item">
|
|
219
|
-
<a class="page-link" href="#" @click.prevent="goToPage(1)">1</a>
|
|
220
|
-
</li>
|
|
221
|
-
<li v-if="currentPage > 5" class="page-item disabled">
|
|
222
|
-
<span class="page-link">...</span>
|
|
223
|
-
</li>
|
|
224
|
-
|
|
225
|
-
<!-- 中间页码 -->
|
|
226
|
-
<li
|
|
227
|
-
v-for="page in visiblePages"
|
|
228
|
-
:key="page"
|
|
229
|
-
class="page-item"
|
|
230
|
-
:class="{ active: currentPage === page }"
|
|
231
|
-
>
|
|
232
|
-
<a class="page-link" href="#" @click.prevent="goToPage(page)">{{ page }}</a>
|
|
233
|
-
</li>
|
|
234
|
-
|
|
235
|
-
<!-- 省略号和最后一页 -->
|
|
236
|
-
<li v-if="currentPage < totalPages - 4" class="page-item disabled">
|
|
237
|
-
<span class="page-link">...</span>
|
|
238
|
-
</li>
|
|
239
|
-
<li v-if="currentPage < totalPages - 3" class="page-item">
|
|
240
|
-
<a class="page-link" href="#" @click.prevent="goToPage(totalPages)">{{ totalPages }}</a>
|
|
241
|
-
</li>
|
|
242
|
-
|
|
243
|
-
<li class="page-item" :class="{ disabled: currentPage === totalPages }">
|
|
244
|
-
<a class="page-link" href="#" @click.prevent="goToPage(currentPage + 1)" title="下一页">
|
|
245
|
-
<i class="bi bi-chevron-right"></i>
|
|
246
|
-
</a>
|
|
247
|
-
</li>
|
|
248
|
-
<li class="page-item" :class="{ disabled: currentPage === totalPages }">
|
|
249
|
-
<a class="page-link" href="#" @click.prevent="goToPage(totalPages)" title="末页">
|
|
250
|
-
<i class="bi bi-chevron-double-right"></i>
|
|
251
|
-
</a>
|
|
252
|
-
</li>
|
|
253
|
-
</ul>
|
|
254
|
-
<div class="page-size-selector">
|
|
255
|
-
<label class="form-label-sm mb-0">每页显示:</label>
|
|
256
|
-
<select class="form-select form-select-sm ms-2" v-model="pageSize" style="width: 80px;">
|
|
257
|
-
<option :value="10">10</option>
|
|
258
|
-
<option :value="20">20</option>
|
|
259
|
-
<option :value="50">50</option>
|
|
260
|
-
<option :value="100">100</option>
|
|
261
|
-
<option :value="200">200</option>
|
|
262
|
-
<option :value="500">500</option>
|
|
263
|
-
</select>
|
|
264
|
-
</div>
|
|
265
|
-
<div class="page-jump">
|
|
266
|
-
<label class="form-label-sm mb-0">跳转到:</label>
|
|
267
|
-
<input
|
|
268
|
-
type="number"
|
|
269
|
-
class="form-control form-control-sm ms-2"
|
|
270
|
-
v-model.number="jumpToPage"
|
|
271
|
-
min="1"
|
|
272
|
-
:max="totalPages"
|
|
273
|
-
style="width: 70px;"
|
|
274
|
-
@keyup.enter="jumpToPageHandler"
|
|
275
|
-
@blur="jumpToPageHandler"
|
|
276
|
-
>
|
|
277
|
-
<button class="btn btn-primary btn-sm ms-2" @click="jumpToPageHandler">
|
|
278
|
-
跳转
|
|
279
|
-
</button>
|
|
280
|
-
</div>
|
|
281
|
-
</div>
|
|
282
|
-
</nav>
|
|
283
|
-
</div>
|
|
142
|
+
<TableDataGrid
|
|
143
|
+
ref="tableDataGridRef"
|
|
144
|
+
:connection="connection"
|
|
145
|
+
:database="database"
|
|
146
|
+
:table="table"
|
|
147
|
+
:columns="tableStructure?.columns || []"
|
|
148
|
+
@edit-row="editRow"
|
|
149
|
+
@delete-row="deleteRow"
|
|
150
|
+
/>
|
|
284
151
|
</div>
|
|
285
152
|
|
|
286
153
|
<!-- 结构标签页 -->
|
|
@@ -475,6 +342,7 @@ import type { ConnectionEntity, TableEntity } from '@/typings/database';
|
|
|
475
342
|
import { DatabaseService } from '@/service/database';
|
|
476
343
|
import DataEditor from './data-editor.vue';
|
|
477
344
|
|
|
345
|
+
import TableDataGrid from './table-data-grid.vue';
|
|
478
346
|
import TableEditor from './table-editor.vue';
|
|
479
347
|
import SqlExecutor from './sql-executor.vue';
|
|
480
348
|
import { exportDataToCSV, exportDataToJSON, exportDataToExcel, formatFileName } from '../utils/export';
|
|
@@ -519,14 +387,12 @@ const emit = defineEmits<{
|
|
|
519
387
|
|
|
520
388
|
const databaseService = new DatabaseService();
|
|
521
389
|
|
|
390
|
+
// 引用
|
|
391
|
+
const tableDataGridRef = ref();
|
|
392
|
+
|
|
522
393
|
// 响应式数据
|
|
523
394
|
const activeTab = ref('data');
|
|
524
|
-
const searchQuery = ref('');
|
|
525
|
-
const currentPage = ref(1);
|
|
526
|
-
const pageSize = ref(50);
|
|
527
395
|
const sqlQuery = ref('');
|
|
528
|
-
const jumpToPage = ref(1);
|
|
529
|
-
const searchTimeout = ref<NodeJS.Timeout | null>(null);
|
|
530
396
|
|
|
531
397
|
// 数据编辑相关
|
|
532
398
|
const showDataEditor = ref(false);
|
|
@@ -538,8 +404,6 @@ const showTableEditor = ref(false);
|
|
|
538
404
|
const tableEditorMode = ref<'create' | 'edit'>('edit');
|
|
539
405
|
|
|
540
406
|
// 计算属性
|
|
541
|
-
const tableColumns = computed(() => props.tableStructure?.columns || []);
|
|
542
|
-
|
|
543
407
|
// 类型安全的表列数据
|
|
544
408
|
const safeTableColumns = computed(() => {
|
|
545
409
|
const columns = props.tableStructure?.columns || [];
|
|
@@ -554,48 +418,9 @@ const safeTableColumns = computed(() => {
|
|
|
554
418
|
}));
|
|
555
419
|
});
|
|
556
420
|
|
|
557
|
-
// 直接使用后端返回的数据,不需要前端分页和过滤
|
|
558
|
-
const paginatedData = computed(() => {
|
|
559
|
-
return props.tableData || [];
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
const totalPages = computed(() => {
|
|
563
|
-
const total = parseInt(props.total) || 0;
|
|
564
|
-
return Math.ceil(total / pageSize.value);
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
const visiblePages = computed(() => {
|
|
568
|
-
const pages: number[] = [];
|
|
569
|
-
let start = Math.max(1, currentPage.value - 2);
|
|
570
|
-
let end = Math.min(totalPages.value, start + 4);
|
|
571
|
-
|
|
572
|
-
// 如果显示的页码数不足5个,调整起始位置
|
|
573
|
-
if (end - start < 4) {
|
|
574
|
-
start = Math.max(1, end - 4);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
for (let i = start; i <= end; i++) {
|
|
578
|
-
pages.push(i);
|
|
579
|
-
}
|
|
580
|
-
return pages;
|
|
581
|
-
});
|
|
582
|
-
|
|
583
421
|
// 监听变化
|
|
584
422
|
watch(() => props.table, () => {
|
|
585
423
|
activeTab.value = 'data';
|
|
586
|
-
currentPage.value = 1;
|
|
587
|
-
searchQuery.value = '';
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
watch(pageSize, () => {
|
|
591
|
-
currentPage.value = 1;
|
|
592
|
-
jumpToPage.value = 1;
|
|
593
|
-
// 调用后端分页接口
|
|
594
|
-
emit('refresh-data', currentPage.value, pageSize.value, searchQuery.value);
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
watch(currentPage, (newPage) => {
|
|
598
|
-
jumpToPage.value = newPage;
|
|
599
424
|
});
|
|
600
425
|
|
|
601
426
|
// 方法
|
|
@@ -611,81 +436,10 @@ function formatNumber(num: number): string {
|
|
|
611
436
|
return num?.toLocaleString?.() || num?.toString() || '';
|
|
612
437
|
}
|
|
613
438
|
|
|
614
|
-
function formatCellValue(value: any): string {
|
|
615
|
-
if (value === null || value === undefined) return 'NULL';
|
|
616
|
-
|
|
617
|
-
// 尝试检测并格式化 JSON 数据
|
|
618
|
-
let strValue = String(value);
|
|
619
|
-
if (typeof value === 'string') {
|
|
620
|
-
// 检查是否可能是 JSON 字符串
|
|
621
|
-
const trimmedValue = strValue.trim();
|
|
622
|
-
if ((trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
|
|
623
|
-
(trimmedValue.startsWith('[') && trimmedValue.endsWith(']'))) {
|
|
624
|
-
try {
|
|
625
|
-
const parsed = JSON.parse(trimmedValue);
|
|
626
|
-
// 格式化 JSON 并限制长度
|
|
627
|
-
const formatted = JSON.stringify(parsed, null, 2);
|
|
628
|
-
if (formatted.length > 50) {
|
|
629
|
-
return formatted.substring(0, 50) + '...';
|
|
630
|
-
}
|
|
631
|
-
return formatted;
|
|
632
|
-
} catch (e) {
|
|
633
|
-
// 不是有效的 JSON,继续处理
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
} else if (typeof value === 'object') {
|
|
637
|
-
// 对于对象或数组类型,直接格式化
|
|
638
|
-
try {
|
|
639
|
-
const formatted = JSON.stringify(value, null, 2);
|
|
640
|
-
if (formatted.length > 50) {
|
|
641
|
-
return formatted.substring(0, 50) + '...';
|
|
642
|
-
}
|
|
643
|
-
return formatted;
|
|
644
|
-
} catch (e) {
|
|
645
|
-
// 格式化失败,继续处理
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// 对于普通字符串,限制显示长度
|
|
650
|
-
if (strValue.length > 50) return strValue.substring(0, 50) + '...';
|
|
651
|
-
|
|
652
|
-
return strValue;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function goToPage(page: number) {
|
|
656
|
-
if (page >= 1 && page <= totalPages.value) {
|
|
657
|
-
currentPage.value = page;
|
|
658
|
-
// 调用后端分页接口
|
|
659
|
-
emit('refresh-data', currentPage.value, pageSize.value, searchQuery.value);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function jumpToPageHandler() {
|
|
664
|
-
if (jumpToPage.value >= 1 && jumpToPage.value <= totalPages.value) {
|
|
665
|
-
currentPage.value = jumpToPage.value;
|
|
666
|
-
// 调用后端分页接口
|
|
667
|
-
emit('refresh-data', currentPage.value, pageSize.value, searchQuery.value);
|
|
668
|
-
} else {
|
|
669
|
-
// 重置到有效范围
|
|
670
|
-
jumpToPage.value = Math.max(1, Math.min(jumpToPage.value, totalPages.value));
|
|
671
|
-
currentPage.value = jumpToPage.value;
|
|
672
|
-
emit('refresh-data', currentPage.value, pageSize.value, searchQuery.value);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function handleSearch() {
|
|
677
|
-
// 使用防抖,避免频繁调用后端接口
|
|
678
|
-
clearTimeout(searchTimeout.value);
|
|
679
|
-
searchTimeout.value = setTimeout(() => {
|
|
680
|
-
currentPage.value = 1;
|
|
681
|
-
jumpToPage.value = 1;
|
|
682
|
-
// 调用后端搜索接口
|
|
683
|
-
emit('refresh-data', currentPage.value, pageSize.value, searchQuery.value);
|
|
684
|
-
}, 500);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
439
|
function refreshData() {
|
|
688
|
-
|
|
440
|
+
if (tableDataGridRef.value) {
|
|
441
|
+
tableDataGridRef.value.refresh();
|
|
442
|
+
}
|
|
689
443
|
}
|
|
690
444
|
|
|
691
445
|
function insertData(newData?: any) {
|
|
@@ -743,10 +497,35 @@ async function deleteRow(row: any) {
|
|
|
743
497
|
});
|
|
744
498
|
|
|
745
499
|
if (result) {
|
|
746
|
-
|
|
500
|
+
// 获取主键条件
|
|
501
|
+
const primaryKeys = props.tableStructure?.columns?.filter((col: any) => col.isPrimary) || [];
|
|
502
|
+
if (primaryKeys.length === 0) {
|
|
503
|
+
await modal.warning('该表没有主键,无法删除单行。');
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const where: any = {};
|
|
508
|
+
primaryKeys.forEach((pk: any) => {
|
|
509
|
+
where[pk.name] = row[pk.name];
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const response = await databaseService.deleteData(
|
|
513
|
+
props.connection?.id || '',
|
|
514
|
+
props.database,
|
|
515
|
+
props.table?.name || '',
|
|
516
|
+
where
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
if (response.ret === 0) {
|
|
520
|
+
await modal.success('删除成功');
|
|
521
|
+
refreshData();
|
|
522
|
+
} else {
|
|
523
|
+
await modal.error('删除失败: ' + (response.msg || '未知错误'));
|
|
524
|
+
}
|
|
747
525
|
}
|
|
748
526
|
} catch (error) {
|
|
749
527
|
console.error('删除行失败:', error);
|
|
528
|
+
modal.error('删除行失败: ' + (error as any).message);
|
|
750
529
|
}
|
|
751
530
|
}
|
|
752
531
|
|
|
@@ -759,7 +538,17 @@ async function truncateTable() {
|
|
|
759
538
|
});
|
|
760
539
|
|
|
761
540
|
if (result) {
|
|
762
|
-
|
|
541
|
+
const response = await databaseService.truncateTable(
|
|
542
|
+
props.connection?.id || '',
|
|
543
|
+
props.database,
|
|
544
|
+
props.table?.name || ''
|
|
545
|
+
);
|
|
546
|
+
if (response.ret === 0) {
|
|
547
|
+
await modal.success('表清空成功');
|
|
548
|
+
refreshData();
|
|
549
|
+
} else {
|
|
550
|
+
await modal.error('清空表失败');
|
|
551
|
+
}
|
|
763
552
|
}
|
|
764
553
|
} catch (error) {
|
|
765
554
|
console.error('清空表失败:', error);
|
|
@@ -808,7 +597,7 @@ function handleDataSubmit(result: any) {
|
|
|
808
597
|
|
|
809
598
|
if (result.ret === 0) {
|
|
810
599
|
// 操作成功,刷新数据
|
|
811
|
-
|
|
600
|
+
refreshData();
|
|
812
601
|
closeDataEditor();
|
|
813
602
|
} else {
|
|
814
603
|
modal.error('操作失败');
|
|
@@ -1193,11 +982,13 @@ function downloadSQLFile(content: string, filename: string) {
|
|
|
1193
982
|
flex: 1;
|
|
1194
983
|
display: flex;
|
|
1195
984
|
flex-direction: column;
|
|
985
|
+
overflow: hidden;
|
|
1196
986
|
}
|
|
1197
987
|
|
|
1198
988
|
.nav-tabs {
|
|
1199
989
|
border-bottom: 1px solid #dee2e6;
|
|
1200
990
|
background-color: #f8f9fa;
|
|
991
|
+
flex-shrink: 0;
|
|
1201
992
|
}
|
|
1202
993
|
|
|
1203
994
|
.nav-tabs .nav-link {
|
|
@@ -1225,10 +1016,16 @@ function downloadSQLFile(content: string, filename: string) {
|
|
|
1225
1016
|
overflow: auto;
|
|
1226
1017
|
padding: 20px;
|
|
1227
1018
|
background-color: #fff;
|
|
1019
|
+
display: flex;
|
|
1020
|
+
flex-direction: column;
|
|
1228
1021
|
}
|
|
1229
1022
|
|
|
1230
1023
|
.tab-panel {
|
|
1024
|
+
flex: 1;
|
|
1025
|
+
display: flex;
|
|
1026
|
+
flex-direction: column;
|
|
1231
1027
|
height: 100%;
|
|
1028
|
+
overflow: auto;
|
|
1232
1029
|
}
|
|
1233
1030
|
|
|
1234
1031
|
.data-content {
|