Просмотр исходного кода

不要组件,直接写了测试通过

qdy 1 месяц назад
Родитель
Сommit
5345de785e

+ 4
- 4
package-lock.json Просмотреть файл

@@ -20,7 +20,7 @@
20 20
         "marked": "^14.1.4",
21 21
         "prismjs": "^1.30.0",
22 22
         "rxjs": "~7.8.0",
23
-        "tabulator-tables": "^5.6.0",
23
+        "tabulator-tables": "^6.3.1",
24 24
         "tslib": "^2.3.0",
25 25
         "zone.js": "~0.15.0"
26 26
       },
@@ -13882,9 +13882,9 @@
13882 13882
       }
13883 13883
     },
13884 13884
     "node_modules/tabulator-tables": {
13885
-      "version": "5.6.1",
13886
-      "resolved": "https://registry.npmjs.org/tabulator-tables/-/tabulator-tables-5.6.1.tgz",
13887
-      "integrity": "sha512-DsmaZqEmlQS/NL5ZJbVtoaeYjJgofEFp+2er7+uwKerGwd/E2rZbeQgux4+Ab1dxNJcbptiX7oUiTwogOnUdgQ==",
13885
+      "version": "6.3.1",
13886
+      "resolved": "https://registry.npmjs.org/tabulator-tables/-/tabulator-tables-6.3.1.tgz",
13887
+      "integrity": "sha512-qFW7kfadtcaISQIibKAIy0f3eeIXUVi8242Vly1iJfMD79kfEGzfczNuPBN/80hDxHzQJXYbmJ8VipI40hQtfA==",
13888 13888
       "license": "MIT"
13889 13889
     },
13890 13890
     "node_modules/tapable": {

+ 1
- 1
package.json Просмотреть файл

@@ -24,7 +24,7 @@
24 24
     "rxjs": "~7.8.0",
25 25
     "tslib": "^2.3.0",
26 26
     "zone.js": "~0.15.0",
27
-    "tabulator-tables": "^5.6.0"
27
+    "tabulator-tables": "^6.3.1"
28 28
   },
29 29
   "peerDependencies": {
30 30
     "ag-grid-angular": "^35.0.0",

+ 1
- 1
projects/base-core/src/lib/components/index.ts Просмотреть файл

@@ -4,4 +4,4 @@ export * from './tree-nav/tree-nav.component';
4 4
 // export * from './sidebar/sidebar.component';
5 5
 // export * from './grid/grid.component';
6 6
 // export * from './grid/grid-demo.component';
7
-export * from './tabulator-table/tabulator-table.component';
7
+export * from './tabulator-grid/tabulator-grid.component';

+ 390
- 0
projects/base-core/src/lib/components/tabulator-grid/README.md Просмотреть файл

@@ -0,0 +1,390 @@
1
+# TabulatorGrid 组件
2
+
3
+极简配置的服务端数据表格组件,只需提供API URL和列定义即可自动实现:
4
+- 服务端分页
5
+- 服务端排序  
6
+- 服务端筛选
7
+- 主题切换
8
+- 响应式设计
9
+
10
+## 安装依赖
11
+
12
+确保项目中已安装 `tabulator-tables`:
13
+
14
+```bash
15
+npm install tabulator-tables@^6.3.1
16
+```
17
+
18
+## 基本使用
19
+
20
+### 1. 在模块中导入组件
21
+
22
+```typescript
23
+import { TabulatorGridComponent } from 'base-core';
24
+
25
+@NgModule({
26
+  imports: [
27
+    TabulatorGridComponent
28
+  ]
29
+})
30
+export class YourModule { }
31
+```
32
+
33
+### 2. 在组件中定义列配置
34
+
35
+```typescript
36
+import { TabulatorGridColumn } from 'base-core';
37
+
38
+@Component({
39
+  // ...
40
+})
41
+export class YourComponent {
42
+  // 列定义
43
+  columns: TabulatorGridColumn[] = [
44
+    { title: 'ID', field: 'id', width: 80, sorter: 'number' },
45
+    { title: '姓名', field: 'name', sorter: 'string', headerFilter: true },
46
+    { title: '年龄', field: 'age', sorter: 'number', headerFilter: 'number' },
47
+    { title: '邮箱', field: 'email', sorter: 'string', headerFilter: true },
48
+    { title: '状态', field: 'status', formatter: 'tickCross', hozAlign: 'center' }
49
+  ];
50
+
51
+  // API地址
52
+  apiUrl = '/api/config/meta/list';
53
+
54
+  // 事件处理
55
+  onSort(event: any) {
56
+    console.log('排序变化:', event);
57
+  }
58
+
59
+  onFilter(event: any) {
60
+    console.log('筛选变化:', event);
61
+  }
62
+
63
+  onRowClick(event: any) {
64
+    console.log('行点击:', event);
65
+  }
66
+}
67
+```
68
+
69
+### 3. 在模板中使用
70
+
71
+```html
72
+<lib-tabulator-grid
73
+  [dataSource]="apiUrl"
74
+  [columns]="columns"
75
+  [pageSize]="20"
76
+  theme="bootstrap4"
77
+  [showHeaderFilters]="true"
78
+  (sortChanged)="onSort($event)"
79
+  (filterChanged)="onFilter($event)"
80
+  (rowClicked)="onRowClick($event)">
81
+</lib-tabulator-grid>
82
+```
83
+
84
+## 高级配置
85
+
86
+### 使用对象数据源
87
+
88
+```typescript
89
+dataSource = {
90
+  url: '/api/config/meta/list',
91
+  method: 'POST', // 默认POST
92
+  headers: {
93
+    'Authorization': 'Bearer token'
94
+  },
95
+  params: {
96
+    tenantId: '123'
97
+  },
98
+  // 自定义请求映射
99
+  requestMapper: (params) => {
100
+    return {
101
+      ...params,
102
+      extraParam: 'value'
103
+    };
104
+  },
105
+  // 自定义响应映射
106
+  responseMapper: (response) => {
107
+    return {
108
+      data: response.data,
109
+      total: response.totalCount,
110
+      last_page: Math.ceil(response.totalCount / 20)
111
+    };
112
+  }
113
+};
114
+```
115
+
116
+### 字段映射和操作符映射
117
+
118
+```typescript
119
+config = {
120
+  fieldMapping: {
121
+    'name': 'user_name',
122
+    'age': 'user_age',
123
+    'email': 'user_email'
124
+  },
125
+  operatorMapping: {
126
+    'contains': 'like',
127
+    'startsWith': 'like',
128
+    'endsWith': 'like'
129
+  }
130
+};
131
+```
132
+
133
+### 完整配置示例
134
+
135
+```typescript
136
+import { TabulatorGridConfig } from 'base-core';
137
+
138
+@Component({
139
+  // ...
140
+})
141
+export class YourComponent {
142
+  columns = [
143
+    { title: 'ID', field: 'id', width: 80, sorter: 'number' },
144
+    { title: '配置名称', field: 'configName', sorter: 'string', headerFilter: true },
145
+    { title: '字段名', field: 'fieldName', sorter: 'string', headerFilter: true },
146
+    { title: '字段类型', field: 'fieldType', sorter: 'string', headerFilter: 'select' },
147
+    { title: 'YAML标签', field: 'yamlName', sorter: 'string', headerFilter: true },
148
+    { title: '创建时间', field: 'createdAt', sorter: 'date', formatter: 'datetime' }
149
+  ];
150
+
151
+  dataSource = {
152
+    url: '/api/tabulator/config/meta/list',
153
+    method: 'POST'
154
+  };
155
+
156
+  config: TabulatorGridConfig = {
157
+    height: '600px',
158
+    layout: 'fitData',
159
+    placeholder: '暂无配置数据',
160
+    fieldMapping: {
161
+      'configName': 'config_name',
162
+      'fieldName': 'field_name', 
163
+      'fieldType': 'field_type',
164
+      'yamlName': 'yaml_name',
165
+      'createdAt': 'created_at'
166
+    },
167
+    operatorMapping: {
168
+      '=': '=',
169
+      'like': 'like',
170
+      '>': 'gt',
171
+      '<': 'lt'
172
+    }
173
+  };
174
+
175
+  pageSize = 20;
176
+  theme = 'bootstrap5';
177
+  showHeaderFilters = true;
178
+}
179
+```
180
+
181
+## 支持的Tabulator主题
182
+
183
+- `default` - 默认主题
184
+- `bootstrap` - Bootstrap 3
185
+- `bootstrap4` - Bootstrap 4 (默认)
186
+- `bootstrap5` - Bootstrap 5
187
+- `materialize` - Materialize
188
+- `semanticui` - Semantic UI
189
+- `bulma` - Bulma
190
+- `simple` - 简约主题
191
+- `midnight` - 深色主题
192
+- `modern` - 现代主题
193
+
194
+## 事件API
195
+
196
+| 事件 | 描述 | 事件数据 |
197
+|------|------|----------|
198
+| `sortChanged` | 排序变化时触发 | `{ sorters: Array<{field, dir}> }` |
199
+| `filterChanged` | 筛选变化时触发 | `{ filters: Array<{field, type, value}> }` |
200
+| `pageChanged` | 分页变化时触发 | `{ page: number, pageSize: number }` |
201
+| `rowClicked` | 行点击时触发 | `{ row: any, event?: Event }` |
202
+| `dataLoaded` | 数据加载完成时触发 | `data: any[]` |
203
+| `loadingStateChanged` | 加载状态变化时触发 | `loading: boolean` |
204
+| `error` | 发生错误时触发 | `error: any` |
205
+
206
+## 公共API方法
207
+
208
+| 方法 | 描述 | 参数 |
209
+|------|------|------|
210
+| `reloadData()` | 重新加载数据 | 无 |
211
+| `loadData()` | 加载数据 | 无 |
212
+| `setFilters(filters)` | 设置筛选器 | `filters: Array<{field, type, value}>` |
213
+| `setSort(sorters)` | 设置排序 | `sorters: Array<{field, dir}>` |
214
+| `getData()` | 获取当前数据 | 无 |
215
+| `getSelectedData()` | 获取选中的数据 | 无 |
216
+| `getCurrentPage()` | 获取当前页码 | 无 |
217
+| `setPage(page)` | 跳转到指定页码 | `page: number` |
218
+| `getTotalPages()` | 获取总页数 | 无 |
219
+
220
+## 后端API要求
221
+
222
+组件期望后端API支持以下格式:
223
+
224
+### 请求格式
225
+```json
226
+{
227
+  "page": 1,
228
+  "size": 20,
229
+  "sorters": [
230
+    { "field": "field_name", "dir": "asc" }
231
+  ],
232
+  "filters": [
233
+    { "field": "config_name", "type": "like", "value": "search" }
234
+  ]
235
+}
236
+```
237
+
238
+### 响应格式
239
+```json
240
+{
241
+  "last_page": 5,
242
+  "data": [
243
+    { "id": 1, "config_name": "database", "field_name": "host", "field_type": "string" }
244
+  ],
245
+  "total": 100
246
+}
247
+```
248
+
249
+## 适配现有后端API
250
+
251
+如果后端API格式不同,可以使用 `requestMapper` 和 `responseMapper` 进行适配:
252
+
253
+```typescript
254
+dataSource = {
255
+  url: '/api/config/meta/list',
256
+  requestMapper: (params) => {
257
+    // 转换为后端需要的格式
258
+    return {
259
+      page: params.page,
260
+      pageSize: params.size,
261
+      sortField: params.sorters[0]?.field,
262
+      sortOrder: params.sorters[0]?.dir,
263
+      filters: params.filters
264
+    };
265
+  },
266
+  responseMapper: (response) => {
267
+    // 转换为Tabulator需要的格式
268
+    return {
269
+      last_page: Math.ceil(response.totalCount / response.pageSize),
270
+      data: response.data,
271
+      total: response.totalCount
272
+    };
273
+  }
274
+};
275
+```
276
+
277
+## 样式定制
278
+
279
+组件提供了一些CSS类用于样式定制:
280
+
281
+```css
282
+/* 紧凑模式 */
283
+.tabulator-grid-compact .tabulator-grid-wrapper { /* ... */ }
284
+
285
+/* 边框模式 */
286
+.tabulator-grid-bordered .tabulator-grid-wrapper { /* ... */ }
287
+
288
+/* 深色主题适配 */
289
+:host-context(.dark-theme) .tabulator-grid-wrapper { /* ... */ }
290
+```
291
+
292
+## 响应式设计
293
+
294
+组件在不同屏幕尺寸下自动适配:
295
+- 在大屏幕上显示完整分页控件
296
+- 在移动设备上简化分页布局
297
+- 自动调整列宽和布局
298
+
299
+## 常见问题
300
+
301
+### 1. 表格不显示数据?
302
+- 检查API地址是否正确
303
+- 检查网络请求是否成功
304
+- 查看浏览器控制台错误信息
305
+- 确认后端API返回正确的格式
306
+
307
+### 2. 排序/筛选不工作?
308
+- 确认后端API支持排序和筛选
309
+- 检查字段映射是否正确
310
+- 查看请求参数格式
311
+
312
+### 3. 样式不正确?
313
+- 确认已导入Tabulator的CSS文件
314
+- 检查主题名称是否正确
315
+- 查看是否有CSS冲突
316
+
317
+### 4. 如何自定义列样式?
318
+- 在列定义中使用 `cssClass` 属性
319
+- 使用Tabulator原生的格式化器
320
+- 通过CSS选择器覆盖样式
321
+
322
+## 示例:配置元数据表格
323
+
324
+这是一个完整的配置元数据表格示例:
325
+
326
+```typescript
327
+import { Component } from '@angular/core';
328
+import { TabulatorGridComponent, TabulatorGridColumn } from 'base-core';
329
+
330
+@Component({
331
+  selector: 'app-config-meta-grid',
332
+  template: `
333
+    <lib-tabulator-grid
334
+      [dataSource]="dataSource"
335
+      [columns]="columns"
336
+      [pageSize]="15"
337
+      [config]="config"
338
+      theme="bootstrap5"
339
+      (rowClicked)="onRowClick($event)">
340
+    </lib-tabulator-grid>
341
+  `,
342
+  imports: [TabulatorGridComponent]
343
+})
344
+export class ConfigMetaGridComponent {
345
+  columns: TabulatorGridColumn[] = [
346
+    { title: 'ID', field: 'id', width: 80, sorter: 'number' },
347
+    { title: '配置名称', field: 'configName', width: 150, sorter: 'string', headerFilter: true },
348
+    { title: '字段名', field: 'fieldName', width: 150, sorter: 'string', headerFilter: true },
349
+    { title: '字段类型', field: 'fieldType', width: 120, sorter: 'string', headerFilter: 'select' },
350
+    { title: 'YAML标签', field: 'yamlName', width: 150, sorter: 'string', headerFilter: true },
351
+    { title: '字段描述', field: 'fieldDesc', width: 200, formatter: 'textarea' },
352
+    { title: '创建者', field: 'creator', width: 120, sorter: 'string' },
353
+    { title: '创建时间', field: 'createdAt', width: 180, sorter: 'date', formatter: 'datetime' }
354
+  ];
355
+
356
+  dataSource = {
357
+    url: '/api/tabulator/config/meta/list',
358
+    method: 'POST'
359
+  };
360
+
361
+  config = {
362
+    height: 'calc(100vh - 200px)',
363
+    layout: 'fitData',
364
+    fieldMapping: {
365
+      'configName': 'config_name',
366
+      'fieldName': 'field_name',
367
+      'fieldType': 'field_type',
368
+      'yamlName': 'yaml_name',
369
+      'fieldDesc': 'field_desc',
370
+      'createdAt': 'created_at'
371
+    }
372
+  };
373
+
374
+  onRowClick(event: any) {
375
+    console.log('选中行:', event.row);
376
+    // 可以在这里打开详情对话框等
377
+  }
378
+}
379
+```
380
+
381
+## 支持的后端框架
382
+
383
+组件已与以下后端框架集成:
384
+
385
+1. **Go (svc-configure)** - 使用 `TabulatorRequest` 和 `CreateTabulatorResponse`
386
+2. **Node.js/Express** - 参考请求/响应格式
387
+3. **Java/Spring Boot** - 参考请求/响应格式
388
+4. **Python/Django/Flask** - 参考请求/响应格式
389
+
390
+具体集成代码请参考项目中的 `svc-configure` 服务实现。

+ 1
- 0
projects/base-core/src/lib/components/tabulator-grid/index.ts Просмотреть файл

@@ -0,0 +1 @@
1
+export * from './tabulator-grid.component';

+ 25
- 0
projects/base-core/src/lib/components/tabulator-grid/tabulator-grid.component.html Просмотреть файл

@@ -0,0 +1,25 @@
1
+<div class="tabulator-grid-wrapper">
2
+  <!-- 加载状态指示器 -->
3
+  <div class="tabulator-grid-loading" *ngIf="isDataLoading">
4
+    <div class="loading-spinner">
5
+      <div class="spinner"></div>
6
+      <div class="loading-text">{{ loadingText }}</div>
7
+    </div>
8
+  </div>
9
+
10
+  <!-- 表格容器 -->
11
+  <div #tableContainer class="tabulator-grid-container"></div>
12
+
13
+  <!-- 空状态提示 -->
14
+  <div class="tabulator-grid-empty" *ngIf="!isDataLoading && (dataCount === 0)">
15
+    <div class="empty-icon">
16
+      <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
17
+        <path d="M3 3h18v18H3zM9 9h6M9 13h6M9 17h4"/>
18
+      </svg>
19
+    </div>
20
+    <div class="empty-text">
21
+      <h3>暂无数据</h3>
22
+      <p>当前表格没有数据,请检查数据源或筛选条件</p>
23
+    </div>
24
+  </div>
25
+</div>

+ 307
- 0
projects/base-core/src/lib/components/tabulator-grid/tabulator-grid.component.scss Просмотреть файл

@@ -0,0 +1,307 @@
1
+@use 'sass:math';
2
+
3
+.tabulator-grid-wrapper {
4
+  position: relative;
5
+  width: 100%;
6
+  height: 100%;
7
+  min-height: 300px;
8
+
9
+  .tabulator-grid-loading {
10
+    position: absolute;
11
+    top: 0;
12
+    left: 0;
13
+    right: 0;
14
+    bottom: 0;
15
+    background: rgba(255, 255, 255, 0.8);
16
+    z-index: 1000;
17
+    display: flex;
18
+    align-items: center;
19
+    justify-content: center;
20
+    backdrop-filter: blur(2px);
21
+
22
+    .loading-spinner {
23
+      text-align: center;
24
+      padding: 2rem;
25
+      background: white;
26
+      border-radius: 8px;
27
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
28
+
29
+      .spinner {
30
+        width: 40px;
31
+        height: 40px;
32
+        border: 3px solid #f3f3f3;
33
+        border-top: 3px solid #3498db;
34
+        border-radius: 50%;
35
+        animation: spin 1s linear infinite;
36
+        margin: 0 auto 1rem;
37
+      }
38
+
39
+      .loading-text {
40
+        color: #666;
41
+        font-size: 0.875rem;
42
+      }
43
+    }
44
+  }
45
+
46
+  .tabulator-grid-container {
47
+    width: 100%;
48
+    height: 100%;
49
+    min-height: 300px;
50
+
51
+    // 覆盖Tabulator默认样式
52
+    .tabulator {
53
+      border: 1px solid #dee2e6;
54
+      border-radius: 4px;
55
+      overflow: hidden;
56
+
57
+      // 表头样式
58
+      .tabulator-header {
59
+        background-color: #f8f9fa;
60
+        border-bottom: 1px solid #dee2e6;
61
+
62
+        .tabulator-col {
63
+          background-color: transparent;
64
+          border-right: 1px solid #dee2e6;
65
+          font-weight: 600;
66
+          color: #495057;
67
+
68
+          &:hover {
69
+            background-color: #e9ecef;
70
+          }
71
+
72
+          .tabulator-col-content {
73
+            padding: 0.75rem 0.5rem;
74
+          }
75
+        }
76
+      }
77
+
78
+      // 表体样式
79
+      .tabulator-tableholder {
80
+        background-color: white;
81
+
82
+        .tabulator-table {
83
+          .tabulator-row {
84
+            border-bottom: 1px solid #dee2e6;
85
+
86
+            &:hover {
87
+              background-color: #f8f9fa;
88
+            }
89
+
90
+            &.tabulator-selected {
91
+              background-color: #e3f2fd;
92
+            }
93
+
94
+            .tabulator-cell {
95
+              padding: 0.5rem;
96
+              border-right: 1px solid #dee2e6;
97
+              color: #212529;
98
+
99
+              &:last-child {
100
+                border-right: none;
101
+              }
102
+            }
103
+          }
104
+        }
105
+      }
106
+
107
+      // 分页样式
108
+      .tabulator-footer {
109
+        background-color: #f8f9fa;
110
+        border-top: 1px solid #dee2e6;
111
+        padding: 0.5rem;
112
+
113
+        .tabulator-paginator {
114
+          display: flex;
115
+          align-items: center;
116
+          justify-content: space-between;
117
+          gap: 1rem;
118
+
119
+          .tabulator-page-size {
120
+            select {
121
+              padding: 0.25rem 0.5rem;
122
+              border: 1px solid #ced4da;
123
+              border-radius: 4px;
124
+              background-color: white;
125
+              color: #495057;
126
+            }
127
+          }
128
+
129
+          .tabulator-pages {
130
+            display: flex;
131
+            align-items: center;
132
+            gap: 0.25rem;
133
+
134
+            .tabulator-page {
135
+              min-width: 2rem;
136
+              height: 2rem;
137
+              display: flex;
138
+              align-items: center;
139
+              justify-content: center;
140
+              border: 1px solid #ced4da;
141
+              border-radius: 4px;
142
+              background-color: white;
143
+              color: #495057;
144
+              cursor: pointer;
145
+              font-size: 0.875rem;
146
+
147
+              &:hover:not(.disabled):not(.active) {
148
+                background-color: #e9ecef;
149
+                border-color: #adb5bd;
150
+              }
151
+
152
+              &.active {
153
+                background-color: #007bff;
154
+                border-color: #007bff;
155
+                color: white;
156
+              }
157
+
158
+              &.disabled {
159
+                opacity: 0.5;
160
+                cursor: not-allowed;
161
+              }
162
+            }
163
+          }
164
+        }
165
+      }
166
+    }
167
+  }
168
+
169
+  .tabulator-grid-empty {
170
+    position: absolute;
171
+    top: 50%;
172
+    left: 50%;
173
+    transform: translate(-50%, -50%);
174
+    text-align: center;
175
+    padding: 3rem;
176
+    max-width: 400px;
177
+    color: #6c757d;
178
+
179
+    .empty-icon {
180
+      margin-bottom: 1.5rem;
181
+
182
+      svg {
183
+        color: #adb5bd;
184
+        margin: 0 auto;
185
+      }
186
+    }
187
+
188
+    .empty-text {
189
+      h3 {
190
+        font-size: 1.25rem;
191
+        font-weight: 600;
192
+        margin-bottom: 0.5rem;
193
+        color: #495057;
194
+      }
195
+
196
+      p {
197
+        font-size: 0.875rem;
198
+        line-height: 1.5;
199
+        margin: 0;
200
+      }
201
+    }
202
+  }
203
+}
204
+
205
+// 主题适配
206
+:host-context(.dark-theme) {
207
+  .tabulator-grid-wrapper {
208
+    .tabulator-grid-loading {
209
+      background: rgba(0, 0, 0, 0.7);
210
+
211
+      .loading-spinner {
212
+        background: #2d3748;
213
+        color: #e2e8f0;
214
+
215
+        .spinner {
216
+          border-color: #4a5568;
217
+          border-top-color: #63b3ed;
218
+        }
219
+
220
+        .loading-text {
221
+          color: #a0aec0;
222
+        }
223
+      }
224
+    }
225
+
226
+    .tabulator-grid-empty {
227
+      color: #a0aec0;
228
+
229
+      .empty-icon svg {
230
+        color: #718096;
231
+      }
232
+
233
+      .empty-text h3 {
234
+        color: #e2e8f0;
235
+      }
236
+    }
237
+  }
238
+}
239
+
240
+// 响应式设计
241
+@media (max-width: 768px) {
242
+  .tabulator-grid-wrapper {
243
+    .tabulator-grid-container {
244
+      .tabulator {
245
+        .tabulator-footer {
246
+          .tabulator-paginator {
247
+            flex-direction: column;
248
+            align-items: stretch;
249
+            gap: 0.75rem;
250
+
251
+            .tabulator-pages {
252
+              justify-content: center;
253
+            }
254
+          }
255
+        }
256
+      }
257
+    }
258
+  }
259
+}
260
+
261
+// 动画
262
+@keyframes spin {
263
+  0% { transform: rotate(0deg); }
264
+  100% { transform: rotate(360deg); }
265
+}
266
+
267
+// 工具类
268
+.tabulator-grid-compact {
269
+  .tabulator-grid-wrapper {
270
+    .tabulator-grid-container {
271
+      .tabulator {
272
+        .tabulator-header {
273
+          .tabulator-col {
274
+            .tabulator-col-content {
275
+              padding: 0.5rem 0.25rem;
276
+            }
277
+          }
278
+        }
279
+
280
+        .tabulator-table {
281
+          .tabulator-row {
282
+            .tabulator-cell {
283
+              padding: 0.375rem;
284
+            }
285
+          }
286
+        }
287
+      }
288
+    }
289
+  }
290
+}
291
+
292
+.tabulator-grid-bordered {
293
+  .tabulator-grid-wrapper {
294
+    .tabulator-grid-container {
295
+      .tabulator {
296
+        border: 1px solid #dee2e6;
297
+        border-radius: 8px;
298
+
299
+        .tabulator-header,
300
+        .tabulator-tableholder,
301
+        .tabulator-footer {
302
+          border: none;
303
+        }
304
+      }
305
+    }
306
+  }
307
+}

+ 73
- 0
projects/base-core/src/lib/components/tabulator-grid/tabulator-grid.component.spec.ts Просмотреть файл

@@ -0,0 +1,73 @@
1
+import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+import { TabulatorGridComponent } from './tabulator-grid.component';
3
+import { provideHttpClientTesting } from '@angular/common/http/testing';
4
+import { provideHttpClient } from '@angular/common/http';
5
+
6
+describe('TabulatorGridComponent', () => {
7
+  let component: TabulatorGridComponent;
8
+  let fixture: ComponentFixture<TabulatorGridComponent>;
9
+
10
+  beforeEach(async () => {
11
+    await TestBed.configureTestingModule({
12
+      imports: [TabulatorGridComponent],
13
+      providers: [
14
+        provideHttpClient(),
15
+        provideHttpClientTesting()
16
+      ]
17
+    }).compileComponents();
18
+
19
+    fixture = TestBed.createComponent(TabulatorGridComponent);
20
+    component = fixture.componentInstance;
21
+    fixture.detectChanges();
22
+  });
23
+
24
+  it('should create', () => {
25
+    expect(component).toBeTruthy();
26
+  });
27
+
28
+  it('should have default properties', () => {
29
+    expect(component.pageSize).toBe(10);
30
+    expect(component.theme).toBe('bootstrap4');
31
+    expect(component.showHeaderFilters).toBe(true);
32
+    expect(component.autoLoad).toBe(true);
33
+    expect(component.loadingText).toBe('加载中...');
34
+    expect(component.columns).toEqual([]);
35
+    expect(component.config).toEqual({});
36
+  });
37
+
38
+  it('should emit loading state changes', () => {
39
+    let loadingState = false;
40
+    component.loadingStateChanged.subscribe(state => {
41
+      loadingState = state;
42
+    });
43
+
44
+    // Note: We can't directly test the private setLoading method
45
+    // This is more of an integration test
46
+    expect(loadingState).toBe(false);
47
+  });
48
+
49
+  it('should handle data source as string', () => {
50
+    component.dataSource = '/api/test';
51
+    expect(component).toBeTruthy();
52
+  });
53
+
54
+  it('should handle data source as object', () => {
55
+    component.dataSource = {
56
+      url: '/api/test',
57
+      method: 'GET'
58
+    };
59
+    expect(component).toBeTruthy();
60
+  });
61
+
62
+  it('should have public API methods', () => {
63
+    expect(component.reloadData).toBeDefined();
64
+    expect(component.setFilters).toBeDefined();
65
+    expect(component.setSort).toBeDefined();
66
+    expect(component.getData).toBeDefined();
67
+    expect(component.getSelectedData).toBeDefined();
68
+    expect(component.loadData).toBeDefined();
69
+    expect(component.getCurrentPage).toBeDefined();
70
+    expect(component.setPage).toBeDefined();
71
+    expect(component.getTotalPages).toBeDefined();
72
+  });
73
+});

+ 656
- 0
projects/base-core/src/lib/components/tabulator-grid/tabulator-grid.component.ts Просмотреть файл

@@ -0,0 +1,656 @@
1
+import { 
2
+  Component, 
3
+  Input, 
4
+  Output, 
5
+  EventEmitter, 
6
+  AfterViewInit, 
7
+  OnChanges, 
8
+  OnDestroy, 
9
+  SimpleChanges,
10
+  ViewChild,
11
+  ElementRef,
12
+  HostBinding
13
+} from '@angular/core';
14
+import { CommonModule } from '@angular/common';
15
+import { HttpClient } from '@angular/common/http';
16
+import { Observable } from 'rxjs';
17
+
18
+// Tabulator类型声明
19
+declare var Tabulator: any;
20
+
21
+/**
22
+ * TabulatorGrid列定义
23
+ */
24
+export interface TabulatorGridColumn {
25
+  title: string;           // 列标题
26
+  field: string;           // 字段名
27
+  width?: number | string; // 列宽度
28
+  sorter?: string;         // 排序类型:'string', 'number', 'date', 'alphanum'
29
+  headerFilter?: boolean | string; // 是否启用表头筛选
30
+  headerFilterPlaceholder?: string; // 筛选器占位符
31
+  formatter?: string | ((cell: any) => any); // 格式化器
32
+  hozAlign?: 'left' | 'center' | 'right'; // 水平对齐
33
+  visible?: boolean;       // 是否可见
34
+  frozen?: boolean;        // 是否冻结列
35
+  tooltip?: string | boolean | ((cell: any) => string); // 工具提示
36
+  cssClass?: string;       // CSS类名
37
+  [key: string]: any;      // 其他Tabulator列属性
38
+}
39
+
40
+/**
41
+ * TabulatorGrid数据源配置
42
+ */
43
+export interface TabulatorGridDataSource {
44
+  url: string;                     // API地址
45
+  method?: 'GET' | 'POST';         // 请求方法,默认POST
46
+  headers?: Record<string, string>; // 请求头
47
+  params?: Record<string, any>;    // 额外参数
48
+  responseMapper?: (response: any) => any; // 响应数据映射函数
49
+  requestMapper?: (params: TabulatorRequestParams) => any; // 请求参数映射函数
50
+}
51
+
52
+/**
53
+ * Tabulator请求参数
54
+ */
55
+export interface TabulatorRequestParams {
56
+  page: number;           // 页码(从1开始)
57
+  size: number;           // 每页大小
58
+  sorters: Array<{       // 排序器数组
59
+    field: string;
60
+    dir: 'asc' | 'desc';
61
+  }>;
62
+  filters: Array<{       // 筛选器数组
63
+    field: string;
64
+    type: string;
65
+    value: any;
66
+  }>;
67
+}
68
+
69
+/**
70
+ * TabulatorGrid事件
71
+ */
72
+export interface TabulatorGridSortEvent {
73
+  sorters: Array<{ field: string; dir: 'asc' | 'desc' }>;
74
+}
75
+
76
+export interface TabulatorGridFilterEvent {
77
+  filters: Array<{ field: string; type: string; value: any }>;
78
+}
79
+
80
+export interface TabulatorGridPageEvent {
81
+  page: number;
82
+  pageSize: number;
83
+}
84
+
85
+export interface TabulatorGridRowEvent<T = any> {
86
+  row: T;
87
+  event?: Event;
88
+}
89
+
90
+/**
91
+ * TabulatorGrid配置
92
+ */
93
+export interface TabulatorGridConfig {
94
+  height?: string | number;  // 表格高度
95
+  layout?: 'fitData' | 'fitColumns' | 'fitDataFill' | 'fitDataStretch' | 'fitDataTable';
96
+  placeholder?: string;      // 空数据占位符
97
+  movableColumns?: boolean;  // 是否可移动列
98
+  movableRows?: boolean;     // 是否可移动行
99
+  selectable?: boolean | number; // 是否可选择行
100
+  rowHeight?: number;        // 行高
101
+  index?: string;           // 索引字段
102
+  dataTree?: boolean;       // 是否启用树形结构
103
+  groupBy?: string;         // 分组字段
104
+  fieldMapping?: Record<string, string>; // 字段映射:前端字段名 -> 后端字段名
105
+  operatorMapping?: Record<string, string>; // 操作符映射:Tabulator操作符 -> 后端操作符
106
+  requestMapper?: (params: TabulatorRequestParams) => any; // 自定义请求参数映射
107
+  responseMapper?: (response: any) => any; // 自定义响应数据映射
108
+}
109
+
110
+/**
111
+ * TabulatorGrid组件
112
+ * 
113
+ * 极简配置的服务端数据表格组件,只需提供API URL和列定义即可自动实现:
114
+ * 1. 服务端分页
115
+ * 2. 服务端排序
116
+ * 3. 服务端筛选
117
+ * 4. 主题切换
118
+ * 
119
+ * @example
120
+ * ```html
121
+ * <lib-tabulator-grid
122
+ *   [apiUrl]="'/api/config/meta/list'"
123
+ *   [columns]="columns"
124
+ *   [pageSize]="20"
125
+ *   theme="bootstrap4"
126
+ *   (sortChanged)="onSort($event)"
127
+ *   (filterChanged)="onFilter($event)">
128
+ * </lib-tabulator-grid>
129
+ * ```
130
+ * 
131
+ * @example
132
+ * ```typescript
133
+ * // 简单列定义
134
+ * columns: TabulatorGridColumn[] = [
135
+ *   { title: 'ID', field: 'id', width: 80, sorter: 'number' },
136
+ *   { title: '姓名', field: 'name', sorter: 'string', headerFilter: true },
137
+ *   { title: '年龄', field: 'age', sorter: 'number', headerFilter: 'number' },
138
+ * ];
139
+ * ```
140
+ */
141
+@Component({
142
+  selector: 'lib-tabulator-grid',
143
+  templateUrl: './tabulator-grid.component.html',
144
+  styleUrls: ['./tabulator-grid.component.scss'],
145
+  imports: [CommonModule],
146
+  standalone: true
147
+})
148
+export class TabulatorGridComponent<T = any> implements AfterViewInit, OnChanges, OnDestroy {
149
+  @ViewChild('tableContainer', { static: true }) tableContainer!: ElementRef;
150
+
151
+  // 核心输入属性
152
+  @Input() columns: TabulatorGridColumn[] = [];
153
+  @Input() dataSource!: TabulatorGridDataSource | string;
154
+  @Input() pageSize: number = 10;
155
+  @Input() theme: string = 'bootstrap4';
156
+  @Input() config: TabulatorGridConfig = {};
157
+  @Input() showHeaderFilters: boolean = true;
158
+  @Input() autoLoad: boolean = true;
159
+  @Input() loadingText: string = '加载中...';
160
+
161
+  // 输出事件
162
+  @Output() sortChanged = new EventEmitter<TabulatorGridSortEvent>();
163
+  @Output() filterChanged = new EventEmitter<TabulatorGridFilterEvent>();
164
+  @Output() pageChanged = new EventEmitter<TabulatorGridPageEvent>();
165
+  @Output() rowClicked = new EventEmitter<TabulatorGridRowEvent<T>>();
166
+  @Output() dataLoaded = new EventEmitter<T[]>();
167
+  @Output() loadingStateChanged = new EventEmitter<boolean>();
168
+  @Output() error = new EventEmitter<any>();
169
+
170
+  @HostBinding('class.tabulator-grid-container') tabulatorGridContainer = true;
171
+
172
+  private tabulator: any;
173
+  private isInitialized = false;
174
+  protected isLoading = false;
175
+
176
+  constructor(private http: HttpClient) {}
177
+
178
+  ngAfterViewInit(): void {
179
+    // 使用setTimeout确保DOM完全渲染后再初始化Tabulator
180
+    setTimeout(() => {
181
+      this.initializeTabulator();
182
+    }, 0);
183
+  }
184
+
185
+  ngOnChanges(changes: SimpleChanges): void {
186
+    if (this.isInitialized && this.tabulator) {
187
+      this.updateTabulator(changes);
188
+    }
189
+  }
190
+
191
+  ngOnDestroy(): void {
192
+    this.destroyTabulator();
193
+  }
194
+
195
+  /**
196
+   * 初始化Tabulator实例
197
+   */
198
+  private initializeTabulator(): void {
199
+    if (this.tabulator || !this.tableContainer?.nativeElement) {
200
+      return;
201
+    }
202
+
203
+    const config = this.buildConfig();
204
+    
205
+    try {
206
+      this.tabulator = new Tabulator(this.tableContainer.nativeElement, config);
207
+      this.bindEvents();
208
+      
209
+      // 监听表格构建完成事件
210
+      this.tabulator.on('tableBuilt', () => {
211
+        this.isInitialized = true;
212
+        
213
+        // 自动加载数据
214
+        if (this.autoLoad) {
215
+          this.loadData();
216
+        }
217
+      });
218
+    } catch (error) {
219
+      console.error('Failed to initialize Tabulator:', error);
220
+      this.error.emit(error);
221
+    }
222
+  }
223
+
224
+  /**
225
+   * 构建Tabulator配置
226
+   */
227
+  private buildConfig(): any {
228
+    const config: any = {
229
+      columns: this.processColumns(),
230
+      theme: this.theme,
231
+      pagination: true,
232
+      paginationMode: 'remote',
233
+      paginationSize: this.pageSize,
234
+      paginationSizeSelector: [10, 20, 50, 100],
235
+      sortMode: 'remote',
236
+      filterMode: 'remote',
237
+      ajaxRequestFunc: (url: string, params: any, config: any) => {
238
+        return this.handleAjaxRequest(params);
239
+      },
240
+      ajaxResponse: (url: string, params: any, response: any) => {
241
+        return this.processResponse(response);
242
+      },
243
+      placeholder: this.config.placeholder || '暂无数据',
244
+      layout: this.config.layout || 'fitColumns',
245
+      height: this.config.height || '500px',
246
+      rowHeight: this.config.rowHeight || 40,
247
+      index: this.config.index || 'id',
248
+      movableColumns: this.config.movableColumns || false,
249
+      movableRows: this.config.movableRows || false,
250
+      selectable: this.config.selectable || false,
251
+      dataTree: this.config.dataTree || false,
252
+      groupBy: this.config.groupBy
253
+    };
254
+
255
+    return config;
256
+  }
257
+
258
+  /**
259
+   * 处理列定义
260
+   */
261
+  private processColumns(): any[] {
262
+    return this.columns.map(column => {
263
+      const colDef: any = { ...column };
264
+
265
+      // 处理表头筛选
266
+      if (this.showHeaderFilters && column.headerFilter !== false) {
267
+        if (column.headerFilter === true || column.headerFilter === undefined) {
268
+          colDef.headerFilter = true;
269
+        } else {
270
+          colDef.headerFilter = column.headerFilter;
271
+        }
272
+        
273
+        if (column.headerFilterPlaceholder) {
274
+          colDef.headerFilterPlaceholder = column.headerFilterPlaceholder;
275
+        }
276
+      }
277
+
278
+      return colDef;
279
+    });
280
+  }
281
+
282
+  /**
283
+   * 处理AJAX请求
284
+   */
285
+  private async handleAjaxRequest(tabulatorParams: any): Promise<any> {
286
+    this.setLoading(true);
287
+    
288
+    try {
289
+      const requestParams = this.buildRequestParams(tabulatorParams);
290
+      const dataSource = this.getDataSourceConfig();
291
+      
292
+      const response = await this.makeRequest(dataSource, requestParams);
293
+      return this.createSuccessResponse(response);
294
+    } catch (error) {
295
+      console.error('TabulatorGrid request failed:', error);
296
+      this.error.emit(error);
297
+      return this.createErrorResponse(error);
298
+    } finally {
299
+      this.setLoading(false);
300
+    }
301
+  }
302
+
303
+  /**
304
+   * 构建请求参数
305
+   */
306
+  private buildRequestParams(tabulatorParams: any): any {
307
+    const request: TabulatorRequestParams = {
308
+      page: tabulatorParams.page || 1,
309
+      size: tabulatorParams.size || this.pageSize,
310
+      sorters: [],
311
+      filters: []
312
+    };
313
+
314
+    // 处理排序
315
+    if (tabulatorParams.sorters && tabulatorParams.sorters.length > 0) {
316
+      request.sorters = tabulatorParams.sorters.map((sorter: any) => ({
317
+        field: this.mapFieldToBackend(sorter.field),
318
+        dir: sorter.dir
319
+      }));
320
+    }
321
+
322
+    // 处理筛选
323
+    if (tabulatorParams.filters && tabulatorParams.filters.length > 0) {
324
+      request.filters = tabulatorParams.filters.map((filter: any) => ({
325
+        field: this.mapFieldToBackend(filter.field),
326
+        type: this.mapOperatorToBackend(filter.type),
327
+        value: this.processFilterValue(filter.value)
328
+      }));
329
+    }
330
+
331
+    // 应用自定义请求映射
332
+    if (this.config.requestMapper) {
333
+      return this.config.requestMapper(request);
334
+    }
335
+
336
+    return request;
337
+  }
338
+
339
+  /**
340
+   * 获取数据源配置
341
+   */
342
+  private getDataSourceConfig(): TabulatorGridDataSource {
343
+    if (typeof this.dataSource === 'string') {
344
+      return {
345
+        url: this.dataSource,
346
+        method: 'POST'
347
+      };
348
+    }
349
+    return this.dataSource;
350
+  }
351
+
352
+  /**
353
+   * 发送请求
354
+   */
355
+  private async makeRequest(dataSource: TabulatorGridDataSource, requestParams: any): Promise<any> {
356
+    const method = dataSource.method || 'POST';
357
+    const url = dataSource.url;
358
+    const headers = dataSource.headers || {};
359
+    const params = { ...(dataSource.params || {}), ...requestParams };
360
+
361
+    if (method === 'GET') {
362
+      return this.http.get(url, { headers, params }).toPromise();
363
+    } else {
364
+      return this.http.post(url, params, { headers }).toPromise();
365
+    }
366
+  }
367
+
368
+  /**
369
+   * 创建成功响应
370
+   */
371
+  private createSuccessResponse(response: any): any {
372
+    // 应用自定义响应映射
373
+    if (this.config.responseMapper) {
374
+      response = this.config.responseMapper(response);
375
+    }
376
+
377
+    // 默认Tabulator响应格式
378
+    const data = response?.data || response;
379
+    const lastPage = response?.last_page || response?.totalPages || 1;
380
+    const totalRecords = response?.total || response?.totalCount || (Array.isArray(data) ? data.length : 0);
381
+
382
+    return {
383
+      last_page: lastPage,
384
+      data: data,
385
+      total: totalRecords
386
+    };
387
+  }
388
+
389
+  /**
390
+   * 创建错误响应
391
+   */
392
+  private createErrorResponse(error: any): any {
393
+    return {
394
+      last_page: 1,
395
+      data: [],
396
+      total: 0,
397
+      error: error?.message || '请求失败'
398
+    };
399
+  }
400
+
401
+  /**
402
+   * 处理响应数据
403
+   */
404
+  private processResponse(response: any): any {
405
+    if (response.error) {
406
+      console.error('TabulatorGrid response error:', response.error);
407
+      return [];
408
+    }
409
+
410
+    const data = response.data || [];
411
+    this.dataLoaded.emit(data);
412
+    return data;
413
+  }
414
+
415
+  /**
416
+   * 处理筛选值
417
+   */
418
+  private processFilterValue(value: any): any {
419
+    if (typeof value === 'string' && value.includes(',') && !value.includes('%')) {
420
+      const values = value.split(',')
421
+        .map(v => v.trim())
422
+        .filter(v => v !== '');
423
+      
424
+      if (values.length > 1) {
425
+        return values.map(v => {
426
+          const num = Number(v);
427
+          return isNaN(num) ? v : num;
428
+        });
429
+      }
430
+    }
431
+    return value;
432
+  }
433
+
434
+  /**
435
+   * 映射字段到后端字段名
436
+   */
437
+  private mapFieldToBackend(field: string): string {
438
+    return this.config.fieldMapping?.[field] || field;
439
+  }
440
+
441
+  /**
442
+   * 映射操作符到后端操作符
443
+   */
444
+  private mapOperatorToBackend(operator: string): string {
445
+    const defaultMapping: Record<string, string> = {
446
+      '=': '=',
447
+      '==': '=',
448
+      '!=': '!=',
449
+      '<>': '!=',
450
+      'like': 'like',
451
+      'contains': 'like',
452
+      '>': '>',
453
+      '<': '<',
454
+      '>=': '>=',
455
+      '<=': '<=',
456
+      'in': 'in'
457
+    };
458
+
459
+    const mapping = this.config.operatorMapping || defaultMapping;
460
+    return mapping[operator] || operator;
461
+  }
462
+
463
+  /**
464
+   * 绑定Tabulator事件
465
+   */
466
+  private bindEvents(): void {
467
+    if (!this.tabulator) return;
468
+
469
+    // 排序事件
470
+    this.tabulator.on('sortChanged', (sorters: any[]) => {
471
+      this.handleSortChanged(sorters);
472
+    });
473
+
474
+    // 筛选事件
475
+    this.tabulator.on('filterChanged', (filters: any[]) => {
476
+      this.handleFilterChanged(filters);
477
+    });
478
+
479
+    // 分页事件
480
+    this.tabulator.on('pageLoaded', (page: number) => {
481
+      this.handlePageChanged(page);
482
+    });
483
+
484
+    // 行点击事件
485
+    this.tabulator.on('rowClick', (e: any, row: any) => {
486
+      this.handleRowClicked(e, row);
487
+    });
488
+  }
489
+
490
+  /**
491
+   * 处理排序变化
492
+   */
493
+  private handleSortChanged(sorters: any[]): void {
494
+    const processedSorters = sorters.map(s => ({
495
+      field: this.mapFieldToBackend(s.field),
496
+      dir: s.dir
497
+    }));
498
+
499
+    this.sortChanged.emit({ sorters: processedSorters });
500
+  }
501
+
502
+  /**
503
+   * 处理筛选变化
504
+   */
505
+  private handleFilterChanged(filters: any[]): void {
506
+    const processedFilters = filters.map(f => ({
507
+      field: this.mapFieldToBackend(f.field),
508
+      type: this.mapOperatorToBackend(f.type),
509
+      value: this.processFilterValue(f.value)
510
+    }));
511
+
512
+    this.filterChanged.emit({ filters: processedFilters });
513
+  }
514
+
515
+  /**
516
+   * 处理分页变化
517
+   */
518
+  private handlePageChanged(page: number): void {
519
+    this.pageChanged.emit({
520
+      page,
521
+      pageSize: this.pageSize
522
+    });
523
+  }
524
+
525
+  /**
526
+   * 处理行点击
527
+   */
528
+  private handleRowClicked(event: any, row: any): void {
529
+    this.rowClicked.emit({
530
+      row: row.getData(),
531
+      event
532
+    });
533
+  }
534
+
535
+  /**
536
+   * 设置加载状态
537
+   */
538
+  private setLoading(loading: boolean): void {
539
+    this.isLoading = loading;
540
+    this.loadingStateChanged.emit(loading);
541
+  }
542
+
543
+  /**
544
+   * 更新Tabulator配置
545
+   */
546
+  private updateTabulator(changes: SimpleChanges): void {
547
+    if (!this.tabulator) return;
548
+
549
+    // 列定义变化
550
+    if (changes['columns']) {
551
+      this.tabulator.setColumns(this.processColumns());
552
+    }
553
+
554
+    // 数据源变化需要重新加载
555
+    if (changes['dataSource'] && this.autoLoad) {
556
+      this.loadData();
557
+    }
558
+
559
+    // 主题或配置变化需要重新初始化
560
+    if (changes['theme'] || changes['config'] || changes['pageSize']) {
561
+      this.destroyTabulator();
562
+      setTimeout(() => this.initializeTabulator(), 0);
563
+    }
564
+
565
+    // 表头筛选显示状态变化
566
+    if (changes['showHeaderFilters']) {
567
+      this.tabulator.setColumns(this.processColumns());
568
+    }
569
+  }
570
+
571
+  /**
572
+   * 销毁Tabulator实例
573
+   */
574
+  private destroyTabulator(): void {
575
+    if (this.tabulator) {
576
+      try {
577
+        this.tabulator.destroy();
578
+      } catch (error) {
579
+        console.warn('Error destroying Tabulator:', error);
580
+      }
581
+      this.tabulator = null;
582
+      this.isInitialized = false;
583
+    }
584
+  }
585
+
586
+  /**
587
+   * 公共API方法
588
+   */
589
+
590
+  /** 重新加载数据 */
591
+  reloadData(): void {
592
+    if (this.tabulator) {
593
+      this.tabulator.setData();
594
+    }
595
+  }
596
+
597
+  /** 设置筛选器 */
598
+  setFilters(filters: Array<{ field: string; type: string; value: any }>): void {
599
+    if (this.tabulator) {
600
+      this.tabulator.setFilter(filters);
601
+    }
602
+  }
603
+
604
+  /** 设置排序 */
605
+  setSort(sorters: Array<{ field: string; dir: 'asc' | 'desc' }>): void {
606
+    if (this.tabulator) {
607
+      this.tabulator.setSort(sorters);
608
+    }
609
+  }
610
+
611
+  /** 获取当前数据 */
612
+  getData(): T[] {
613
+    return this.tabulator?.getData() || [];
614
+  }
615
+
616
+  /** 获取选中的数据 */
617
+  getSelectedData(): T[] {
618
+    return this.tabulator?.getSelectedData() || [];
619
+  }
620
+
621
+  /** 加载数据 */
622
+  loadData(): void {
623
+    if (this.tabulator) {
624
+      this.tabulator.setData();
625
+    }
626
+  }
627
+
628
+  /** 获取当前页码 */
629
+  getCurrentPage(): number {
630
+    return this.tabulator?.getPage() || 1;
631
+  }
632
+
633
+  /** 跳转到指定页码 */
634
+  setPage(page: number): void {
635
+    if (this.tabulator) {
636
+      this.tabulator.setPage(page);
637
+    }
638
+  }
639
+
640
+  /** 获取总页数 */
641
+  getTotalPages(): number {
642
+    const data = this.tabulator?.getData() || [];
643
+    const total = this.tabulator?.getDataCount() || 0;
644
+    return Math.ceil(total / this.pageSize);
645
+  }
646
+
647
+  /** 获取数据总数(公共getter) */
648
+  get dataCount(): number {
649
+    return this.tabulator?.getDataCount() || 0;
650
+  }
651
+
652
+  /** 是否正在加载(公共getter) */
653
+  get isDataLoading(): boolean {
654
+    return this.isLoading;
655
+  }
656
+}

+ 0
- 1
projects/base-core/src/lib/components/tabulator-table/tabulator-table.component.html Просмотреть файл

@@ -1 +0,0 @@
1
-<div #tableContainer class="tabulator-container"></div>

+ 0
- 27
projects/base-core/src/lib/components/tabulator-table/tabulator-table.component.scss Просмотреть файл

@@ -1,27 +0,0 @@
1
-:host {
2
-  display: block;
3
-  width: 100%;
4
-  height: 100%;
5
-}
6
-
7
-.tabulator-container {
8
-  width: 100%;
9
-  height: 100%;
10
-  
11
-  // 确保Tabulator表格正确显示
12
-  .tabulator {
13
-    font-family: inherit;
14
-  }
15
-  
16
-  // 覆盖默认样式以适应Angular Material主题
17
-  .tabulator-table {
18
-    background-color: transparent;
19
-  }
20
-  
21
-  // 响应式调整
22
-  @media (max-width: 768px) {
23
-    .tabulator {
24
-      font-size: 12px;
25
-    }
26
-  }
27
-}

+ 0
- 182
projects/base-core/src/lib/components/tabulator-table/tabulator-table.component.spec.ts Просмотреть файл

@@ -1,182 +0,0 @@
1
-import { ComponentFixture, TestBed } from '@angular/core/testing';
2
-import { TabulatorTableComponent } from './tabulator-table.component';
3
-
4
-describe('TabulatorTableComponent', () => {
5
-  let component: TabulatorTableComponent;
6
-  let fixture: ComponentFixture<TabulatorTableComponent>;
7
-
8
-  beforeEach(async () => {
9
-    await TestBed.configureTestingModule({
10
-      declarations: [TabulatorTableComponent]
11
-    })
12
-    .compileComponents();
13
-
14
-    fixture = TestBed.createComponent(TabulatorTableComponent);
15
-    component = fixture.componentInstance;
16
-    fixture.detectChanges();
17
-  });
18
-
19
-  it('should create', () => {
20
-    expect(component).toBeTruthy();
21
-  });
22
-
23
-  it('should initialize with default values', () => {
24
-    expect(component.columns).toEqual([]);
25
-    expect(component.options).toEqual({});
26
-    expect(component.ajaxUrl).toBe('');
27
-    expect(component.pagination).toBe(false);
28
-    expect(component.paginationSize).toBe(10);
29
-    expect(component.sortMode).toBe('local');
30
-    expect(component.filterMode).toBe('local');
31
-  });
32
-
33
-  it('should emit tableBuilt event when initialized', () => {
34
-    spyOn(component.tableBuilt, 'emit');
35
-    
36
-    // 模拟Tabulator初始化
37
-    component['initializeTabulator']();
38
-    
39
-    // 由于Tabulator未加载,可能不会触发事件
40
-    // 这里主要是测试组件结构
41
-    expect(component.tableBuilt.emit).not.toHaveBeenCalled();
42
-  });
43
-
44
-  it('should handle data changes', () => {
45
-    const testData = [{ id: 1, name: 'Test' }];
46
-    component.data = testData;
47
-    
48
-    // 触发ngOnChanges
49
-    component.ngOnChanges({
50
-      data: {
51
-        currentValue: testData,
52
-        previousValue: [],
53
-        firstChange: false,
54
-        isFirstChange: () => false
55
-      }
56
-    });
57
-    
58
-    expect(component.data).toEqual(testData);
59
-  });
60
-
61
-  it('should handle column changes', () => {
62
-    const testColumns = [{ field: 'id', title: 'ID' }];
63
-    component.columns = testColumns;
64
-    
65
-    component.ngOnChanges({
66
-      columns: {
67
-        currentValue: testColumns,
68
-        previousValue: [],
69
-        firstChange: false,
70
-        isFirstChange: () => false
71
-      }
72
-    });
73
-    
74
-    expect(component.columns).toEqual(testColumns);
75
-  });
76
-
77
-  it('should destroy tabulator instance', () => {
78
-    // 模拟Tabulator实例
79
-    component['tabulator'] = {
80
-      destroy: jasmine.createSpy('destroy')
81
-    };
82
-    
83
-    component.ngOnDestroy();
84
-    
85
-    expect(component['tabulator']).toBeNull();
86
-    expect(component['isInitialized']).toBeFalse();
87
-  });
88
-
89
-  describe('public API methods', () => {
90
-    beforeEach(() => {
91
-      // 模拟Tabulator实例
92
-      component['tabulator'] = {
93
-        setData: jasmine.createSpy('setData'),
94
-        setColumns: jasmine.createSpy('setColumns'),
95
-        setPageSize: jasmine.createSpy('setPageSize'),
96
-        setPage: jasmine.createSpy('setPage'),
97
-        getPage: jasmine.createSpy('getPage').and.returnValue(1),
98
-        getPageMax: jasmine.createSpy('getPageMax').and.returnValue(5),
99
-        getFilters: jasmine.createSpy('getFilters').and.returnValue([]),
100
-        setFilter: jasmine.createSpy('setFilter'),
101
-        getSorters: jasmine.createSpy('getSorters').and.returnValue([]),
102
-        setSort: jasmine.createSpy('setSort'),
103
-        clearData: jasmine.createSpy('clearData'),
104
-        addRow: jasmine.createSpy('addRow'),
105
-        updateRow: jasmine.createSpy('updateRow'),
106
-        deleteRow: jasmine.createSpy('deleteRow'),
107
-        getSelectedRows: jasmine.createSpy('getSelectedRows').and.returnValue([]),
108
-        getSelectedData: jasmine.createSpy('getSelectedData').and.returnValue([]),
109
-        download: jasmine.createSpy('download'),
110
-        print: jasmine.createSpy('print')
111
-      };
112
-    });
113
-
114
-    it('should get tabulator instance', () => {
115
-      expect(component.getTabulator()).toBe(component['tabulator']);
116
-    });
117
-
118
-    it('should reload data', () => {
119
-      component.reloadData();
120
-      expect(component['tabulator'].setData).toHaveBeenCalled();
121
-    });
122
-
123
-    it('should set data', () => {
124
-      const data = [{ id: 1 }];
125
-      component.setData(data);
126
-      expect(component['tabulator'].setData).toHaveBeenCalledWith(data);
127
-    });
128
-
129
-    it('should set columns', () => {
130
-      const columns = [{ field: 'id' }];
131
-      component.setColumns(columns);
132
-      expect(component['tabulator'].setColumns).toHaveBeenCalledWith(columns);
133
-    });
134
-
135
-    it('should set page size', () => {
136
-      component.setPageSize(20);
137
-      expect(component['tabulator'].setPageSize).toHaveBeenCalledWith(20);
138
-    });
139
-
140
-    it('should set page', () => {
141
-      component.setPage(2);
142
-      expect(component['tabulator'].setPage).toHaveBeenCalledWith(2);
143
-    });
144
-
145
-    it('should get page', () => {
146
-      expect(component.getPage()).toBe(1);
147
-      expect(component['tabulator'].getPage).toHaveBeenCalled();
148
-    });
149
-
150
-    it('should get max page', () => {
151
-      expect(component.getPageMax()).toBe(5);
152
-      expect(component['tabulator'].getPageMax).toHaveBeenCalled();
153
-    });
154
-
155
-    it('should get filters', () => {
156
-      expect(component.getFilters()).toEqual([]);
157
-      expect(component['tabulator'].getFilters).toHaveBeenCalled();
158
-    });
159
-
160
-    it('should set filters', () => {
161
-      const filters = [{ field: 'id', type: '=', value: 1 }];
162
-      component.setFilters(filters, true);
163
-      expect(component['tabulator'].setFilter).toHaveBeenCalledWith(filters, true);
164
-    });
165
-
166
-    it('should get sorters', () => {
167
-      expect(component.getSorters()).toEqual([]);
168
-      expect(component['tabulator'].getSorters).toHaveBeenCalled();
169
-    });
170
-
171
-    it('should set sorters', () => {
172
-      const sorters = [{ column: 'id', dir: 'asc' }];
173
-      component.setSorters(sorters);
174
-      expect(component['tabulator'].setSort).toHaveBeenCalledWith(sorters);
175
-    });
176
-
177
-    it('should clear data', () => {
178
-      component.clearData();
179
-      expect(component['tabulator'].clearData).toHaveBeenCalled();
180
-    });
181
-  });
182
-});

+ 0
- 372
projects/base-core/src/lib/components/tabulator-table/tabulator-table.component.ts Просмотреть файл

@@ -1,372 +0,0 @@
1
-import { 
2
-  Component, 
3
-  Input, 
4
-  Output, 
5
-  EventEmitter, 
6
-  AfterViewInit, 
7
-  OnChanges, 
8
-  OnDestroy, 
9
-  SimpleChanges,
10
-  ViewChild,
11
-  ElementRef 
12
-} from '@angular/core';
13
-
14
-// Tabulator类型声明
15
-declare var Tabulator: any;
16
-
17
-@Component({
18
-  selector: 'lib-tabulator-table',
19
-  templateUrl: './tabulator-table.component.html',
20
-  styleUrls: ['./tabulator-table.component.scss']
21
-})
22
-export class TabulatorTableComponent implements AfterViewInit, OnChanges, OnDestroy {
23
-  @ViewChild('tableContainer', { static: true }) tableContainer!: ElementRef;
24
-
25
-  // 输入属性 - 完全遵循Tabulator配置
26
-  @Input() columns: any[] = [];
27
-  @Input() options: any = {};
28
-  @Input() ajaxUrl: string = '';
29
-  @Input() ajaxParams: any = {};
30
-  @Input() ajaxConfig: any = {};
31
-  @Input() pagination: boolean | string = false;
32
-  @Input() paginationSize: number = 10;
33
-  @Input() paginationSizeSelector: boolean | number[] = false;
34
-  @Input() sortMode: string = 'local';
35
-  @Input() filterMode: string = 'local';
36
-  @Input() height: string | number = '';
37
-  @Input() layout: string = 'fitColumns';
38
-  @Input() data: any[] = [];
39
-  @Input() index: string = 'id';
40
-  @Input() movableColumns: boolean = false;
41
-  @Input() movableRows: boolean = false;
42
-  @Input() selectable: boolean | number = false;
43
-  @Input() rowHeight: number = 40;
44
-  @Input() placeholder: string = 'No Data Available';
45
-
46
-  // 输出事件 - 映射Tabulator事件
47
-  @Output() tableBuilt = new EventEmitter<any>();
48
-  @Output() dataLoaded = new EventEmitter<any>();
49
-  @Output() dataLoading = new EventEmitter<any>();
50
-  @Output() dataLoadError = new EventEmitter<any>();
51
-  @Output() pageLoaded = new EventEmitter<any>();
52
-  @Output() rowClick = new EventEmitter<any>();
53
-  @Output() rowDblClick = new EventEmitter<any>();
54
-  @Output() cellClick = new EventEmitter<any>();
55
-  @Output() sortChanged = new EventEmitter<any>();
56
-  @Output() filterChanged = new EventEmitter<any>();
57
-  @Output() rowSelected = new EventEmitter<any>();
58
-  @Output() rowDeselected = new EventEmitter<any>();
59
-  @Output() rowContext = new EventEmitter<any>();
60
-  @Output() columnMoved = new EventEmitter<any>();
61
-  @Output() columnResized = new EventEmitter<any>();
62
-  @Output() rowMoved = new EventEmitter<any>();
63
-
64
-  private tabulator: any;
65
-  private isInitialized = false;
66
-
67
-  constructor() {}
68
-
69
-  ngAfterViewInit(): void {
70
-    this.initializeTabulator();
71
-  }
72
-
73
-  ngOnChanges(changes: SimpleChanges): void {
74
-    if (this.isInitialized && this.tabulator) {
75
-      this.updateTabulator(changes);
76
-    }
77
-  }
78
-
79
-  ngOnDestroy(): void {
80
-    this.destroyTabulator();
81
-  }
82
-
83
-  /**
84
-   * 初始化Tabulator实例
85
-   */
86
-  private initializeTabulator(): void {
87
-    if (this.tabulator || !this.tableContainer?.nativeElement) {
88
-      return;
89
-    }
90
-
91
-    const config = this.buildConfig();
92
-    
93
-    try {
94
-      this.tabulator = new Tabulator(this.tableContainer.nativeElement, config);
95
-      this.bindEvents();
96
-      this.isInitialized = true;
97
-      this.tableBuilt.emit(this.tabulator);
98
-    } catch (error) {
99
-      console.error('Failed to initialize Tabulator:', error);
100
-    }
101
-  }
102
-
103
-  /**
104
-   * 构建Tabulator配置
105
-   */
106
-  private buildConfig(): any {
107
-    // 基础配置
108
-    const config: any = {
109
-      columns: this.columns,
110
-      data: this.data,
111
-      index: this.index,
112
-      layout: this.layout,
113
-      movableColumns: this.movableColumns,
114
-      movableRows: this.movableRows,
115
-      selectable: this.selectable,
116
-      rowHeight: this.rowHeight,
117
-      placeholder: this.placeholder,
118
-      ...this.options // 允许覆盖所有配置
119
-    };
120
-
121
-    // 高度设置
122
-    if (this.height) {
123
-      config.height = this.height;
124
-    }
125
-
126
-    // 服务端配置
127
-    if (this.ajaxUrl) {
128
-      config.ajaxURL = this.ajaxUrl;
129
-      config.ajaxParams = this.ajaxParams;
130
-      config.ajaxConfig = this.ajaxConfig;
131
-      
132
-      // 强制服务端模式
133
-      config.pagination = this.pagination || 'remote';
134
-      config.sortMode = 'remote';
135
-      config.filterMode = 'remote';
136
-      
137
-      // 分页配置
138
-      if (config.pagination === 'remote' || config.pagination === true) {
139
-        config.paginationSize = this.paginationSize;
140
-        config.paginationSizeSelector = this.paginationSizeSelector;
141
-        
142
-        // 自定义响应处理器
143
-        config.ajaxResponse = (url: string, params: any, response: any) => {
144
-          // 期望服务端返回格式: { last_page: totalPages, data: [] }
145
-          // 如果格式不同,可以通过options覆盖
146
-          return response;
147
-        };
148
-      }
149
-    } else if (this.pagination) {
150
-      // 客户端分页
151
-      config.pagination = this.pagination;
152
-      config.paginationSize = this.paginationSize;
153
-      config.paginationSizeSelector = this.paginationSizeSelector;
154
-      config.sortMode = this.sortMode;
155
-      config.filterMode = this.filterMode;
156
-    }
157
-
158
-    return config;
159
-  }
160
-
161
-  /**
162
-   * 绑定Tabulator事件
163
-   */
164
-  private bindEvents(): void {
165
-    if (!this.tabulator) return;
166
-
167
-    // 数据事件
168
-    this.tabulator.on('dataLoaded', (data: any) => this.dataLoaded.emit(data));
169
-    this.tabulator.on('dataLoading', (url: string, config: any, params: any) => 
170
-      this.dataLoading.emit({ url, config, params }));
171
-    this.tabulator.on('dataLoadError', (error: any) => this.dataLoadError.emit(error));
172
-    this.tabulator.on('pageLoaded', (page: number) => this.pageLoaded.emit(page));
173
-
174
-    // 交互事件
175
-    this.tabulator.on('rowClick', (e: any, row: any) => this.rowClick.emit({ event: e, row }));
176
-    this.tabulator.on('rowDblClick', (e: any, row: any) => this.rowDblClick.emit({ event: e, row }));
177
-    this.tabulator.on('cellClick', (e: any, cell: any) => this.cellClick.emit({ event: e, cell }));
178
-    this.tabulator.on('rowSelected', (row: any) => this.rowSelected.emit(row));
179
-    this.tabulator.on('rowDeselected', (row: any) => this.rowDeselected.emit(row));
180
-    this.tabulator.on('rowContext', (e: any, row: any) => this.rowContext.emit({ event: e, row }));
181
-
182
-    // 排序和筛选事件
183
-    this.tabulator.on('sortChanged', (sorters: any[]) => this.sortChanged.emit(sorters));
184
-    this.tabulator.on('filterChanged', (filters: any[]) => this.filterChanged.emit(filters));
185
-
186
-    // 列和行操作事件
187
-    this.tabulator.on('columnMoved', (column: any, columns: any[]) => this.columnMoved.emit({ column, columns }));
188
-    this.tabulator.on('columnResized', (column: any) => this.columnResized.emit(column));
189
-    this.tabulator.on('rowMoved', (row: any) => this.rowMoved.emit(row));
190
-  }
191
-
192
-  /**
193
-   * 更新Tabulator配置
194
-   */
195
-  private updateTabulator(changes: SimpleChanges): void {
196
-    if (!this.tabulator) return;
197
-
198
-    // 数据更新
199
-    if (changes['data'] && changes['data'].currentValue !== changes['data'].previousValue) {
200
-      this.tabulator.setData(changes['data'].currentValue);
201
-    }
202
-
203
-    // 列定义更新
204
-    if (changes['columns'] && changes['columns'].currentValue !== changes['columns'].previousValue) {
205
-      this.tabulator.setColumns(changes['columns'].currentValue);
206
-    }
207
-
208
-    // 分页大小更新
209
-    if (changes['paginationSize'] && changes['paginationSize'].currentValue !== changes['paginationSize'].previousValue) {
210
-      this.tabulator.setPageSize(changes['paginationSize'].currentValue);
211
-    }
212
-
213
-    // 高度更新
214
-    if (changes['height'] && changes['height'].currentValue !== changes['height'].previousValue) {
215
-      this.tabulator.setHeight(changes['height'].currentValue);
216
-    }
217
-
218
-    // 其他配置更新需要重新初始化
219
-    const configProps = ['ajaxUrl', 'ajaxParams', 'ajaxConfig', 'pagination', 'sortMode', 'filterMode', 'layout'];
220
-    const needsReinit = configProps.some(prop => changes[prop]);
221
-    
222
-    if (needsReinit) {
223
-      this.destroyTabulator();
224
-      setTimeout(() => this.initializeTabulator(), 0);
225
-    }
226
-  }
227
-
228
-  /**
229
-   * 销毁Tabulator实例
230
-   */
231
-  private destroyTabulator(): void {
232
-    if (this.tabulator) {
233
-      try {
234
-        this.tabulator.destroy();
235
-      } catch (error) {
236
-        console.warn('Error destroying Tabulator:', error);
237
-      }
238
-      this.tabulator = null;
239
-      this.isInitialized = false;
240
-    }
241
-  }
242
-
243
-  /**
244
-   * 公共API方法 - 暴露Tabulator实例方法
245
-   */
246
-  
247
-  // 获取Tabulator实例
248
-  getTabulator(): any {
249
-    return this.tabulator;
250
-  }
251
-
252
-  // 重新加载数据
253
-  reloadData(): void {
254
-    if (this.tabulator) {
255
-      this.tabulator.setData(this.ajaxUrl ? this.ajaxUrl : this.data);
256
-    }
257
-  }
258
-
259
-  // 设置数据
260
-  setData(data: any[] | string): void {
261
-    if (this.tabulator) {
262
-      this.tabulator.setData(data);
263
-    }
264
-  }
265
-
266
-  // 设置列
267
-  setColumns(columns: any[]): void {
268
-    if (this.tabulator) {
269
-      this.tabulator.setColumns(columns);
270
-    }
271
-  }
272
-
273
-  // 设置分页大小
274
-  setPageSize(size: number): void {
275
-    if (this.tabulator) {
276
-      this.tabulator.setPageSize(size);
277
-    }
278
-  }
279
-
280
-  // 跳转到指定页
281
-  setPage(page: number | string): void {
282
-    if (this.tabulator) {
283
-      this.tabulator.setPage(page);
284
-    }
285
-  }
286
-
287
-  // 获取当前页
288
-  getPage(): number {
289
-    return this.tabulator?.getPage() || 1;
290
-  }
291
-
292
-  // 获取总页数
293
-  getPageMax(): number {
294
-    return this.tabulator?.getPageMax() || 1;
295
-  }
296
-
297
-  // 获取筛选器
298
-  getFilters(): any[] {
299
-    return this.tabulator?.getFilters() || [];
300
-  }
301
-
302
-  // 设置筛选器
303
-  setFilters(filters: any[], matchAll?: boolean): void {
304
-    if (this.tabulator) {
305
-      this.tabulator.setFilter(filters, matchAll);
306
-    }
307
-  }
308
-
309
-  // 获取排序器
310
-  getSorters(): any[] {
311
-    return this.tabulator?.getSorters() || [];
312
-  }
313
-
314
-  // 设置排序器
315
-  setSorters(sorters: any[]): void {
316
-    if (this.tabulator) {
317
-      this.tabulator.setSort(sorters);
318
-    }
319
-  }
320
-
321
-  // 清空所有数据
322
-  clearData(): void {
323
-    if (this.tabulator) {
324
-      this.tabulator.clearData();
325
-    }
326
-  }
327
-
328
-  // 添加行
329
-  addRow(data: any, addToTop?: boolean, position?: number): void {
330
-    if (this.tabulator) {
331
-      this.tabulator.addRow(data, addToTop, position);
332
-    }
333
-  }
334
-
335
-  // 更新行
336
-  updateRow(row: any, data: any): void {
337
-    if (this.tabulator) {
338
-      this.tabulator.updateRow(row, data);
339
-    }
340
-  }
341
-
342
-  // 删除行
343
-  deleteRow(row: any): void {
344
-    if (this.tabulator) {
345
-      this.tabulator.deleteRow(row);
346
-    }
347
-  }
348
-
349
-  // 获取选中的行
350
-  getSelectedRows(): any[] {
351
-    return this.tabulator?.getSelectedRows() || [];
352
-  }
353
-
354
-  // 获取选中的数据
355
-  getSelectedData(): any[] {
356
-    return this.tabulator?.getSelectedData() || [];
357
-  }
358
-
359
-  // 下载表格数据
360
-  download(format: string, filename: string, options?: any): void {
361
-    if (this.tabulator) {
362
-      this.tabulator.download(format, filename, options);
363
-    }
364
-  }
365
-
366
-  // 打印表格
367
-  print(): void {
368
-    if (this.tabulator) {
369
-      this.tabulator.print();
370
-    }
371
-  }
372
-}

+ 0
- 4
projects/base-core/src/tabulator-tables.d.ts Просмотреть файл

@@ -1,4 +0,0 @@
1
-declare module 'tabulator-tables' {
2
-  const Tabulator: any;
3
-  export = Tabulator;
4
-}

+ 1
- 0
projects/base-core/tsconfig.lib.json Просмотреть файл

@@ -4,6 +4,7 @@
4 4
     "outDir": "../../out-tsc/lib",
5 5
     "declaration": true,
6 6
     "declarationMap": true,
7
+    "sourceMap": true,
7 8
     "inlineSources": true,
8 9
     "types": []
9 10
   },

Загрузка…
Отмена
Сохранить