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.
@@ -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="tableData.length > 0">
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="tableData.length > 0">{{ tableData.length }}</span>
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
- <div class="data-content" :class="{ 'loading': loading }">
143
- <div class="table-responsive" v-if="!loading && paginatedData.length > 0">
144
- <table class="table table-sm table-striped table-hover">
145
- <thead class="table-light">
146
- <tr>
147
- <th v-for="column in safeTableColumns" :key="column.name">
148
- <div class="column-header">
149
- <span>{{ column.name }}</span>
150
- <small class="text-muted d-block">{{ column.type }}</small>
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
- emit('refresh-data', currentPage.value, pageSize.value, searchQuery.value);
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
- emit('delete-row', row);
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
- emit('truncate-table');
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
- emit('refresh-data');
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 {