Przeglądaj źródła

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

qdy 1 miesiąc temu
rodzic
commit
514edc5a33

+ 4
- 4
angular.json Wyświetl plik

@@ -35,10 +35,10 @@
35 35
                 "input": "src/assets"
36 36
               }
37 37
             ],
38
-            "styles": [
39
-              "src/styles.scss",
40
-              "node_modules/tabulator-tables/dist/css/tabulator.min.css"
41
-            ],
38
+             "styles": [
39
+               "src/styles.scss",
40
+               "node_modules/tabulator-tables/dist/css/tabulator_semanticui.min.css"
41
+             ],
42 42
             "scripts": [
43 43
               "node_modules/tabulator-tables/dist/js/tabulator.min.js"
44 44
             ],

+ 12
- 4
package-lock.json Wyświetl plik

@@ -25,7 +25,7 @@
25 25
         "marked": "^17.0.1",
26 26
         "prismjs": "^1.30.0",
27 27
         "rxjs": "~7.8.0",
28
-        "tabulator-tables": "^5.6.0",
28
+        "tabulator-tables": "^6.3.1",
29 29
         "tslib": "^2.3.0",
30 30
         "zone.js": "~0.15.0"
31 31
       },
@@ -35,6 +35,7 @@
35 35
         "@angular/compiler-cli": "^19.2.0",
36 36
         "@types/jasmine": "~5.1.0",
37 37
         "@types/prismjs": "^1.26.5",
38
+        "@types/tabulator-tables": "^6.3.1",
38 39
         "autoprefixer": "^10.4.23",
39 40
         "jasmine-core": "~5.6.0",
40 41
         "karma": "~6.4.0",
@@ -5876,6 +5877,13 @@
5876 5877
         "@types/node": "*"
5877 5878
       }
5878 5879
     },
5880
+    "node_modules/@types/tabulator-tables": {
5881
+      "version": "6.3.1",
5882
+      "resolved": "https://registry.npmjs.org/@types/tabulator-tables/-/tabulator-tables-6.3.1.tgz",
5883
+      "integrity": "sha512-qL05wGXVy0yfWcF8LCE9+9uSeUIpeKdgpm8YmOAPTjLd3FaoZziPOhVxIiLzEhLTFfOvbuwnaWDm4v4i87diRQ==",
5884
+      "dev": true,
5885
+      "license": "MIT"
5886
+    },
5879 5887
     "node_modules/@types/ws": {
5880 5888
       "version": "8.18.1",
5881 5889
       "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -13969,9 +13977,9 @@
13969 13977
       }
13970 13978
     },
13971 13979
     "node_modules/tabulator-tables": {
13972
-      "version": "5.6.1",
13973
-      "resolved": "https://registry.npmjs.org/tabulator-tables/-/tabulator-tables-5.6.1.tgz",
13974
-      "integrity": "sha512-DsmaZqEmlQS/NL5ZJbVtoaeYjJgofEFp+2er7+uwKerGwd/E2rZbeQgux4+Ab1dxNJcbptiX7oUiTwogOnUdgQ==",
13980
+      "version": "6.3.1",
13981
+      "resolved": "https://registry.npmjs.org/tabulator-tables/-/tabulator-tables-6.3.1.tgz",
13982
+      "integrity": "sha512-qFW7kfadtcaISQIibKAIy0f3eeIXUVi8242Vly1iJfMD79kfEGzfczNuPBN/80hDxHzQJXYbmJ8VipI40hQtfA==",
13975 13983
       "license": "MIT"
13976 13984
     },
13977 13985
     "node_modules/tailwindcss": {

+ 3
- 2
package.json Wyświetl plik

@@ -23,11 +23,11 @@
23 23
     "@types/marked": "^5.0.2",
24 24
     "ag-grid-angular": "^35.0.0",
25 25
     "ag-grid-community": "^35.0.0",
26
-    "tabulator-tables": "^5.6.0",
27
-    "marked": "^17.0.1",
28 26
     "base-core": "file:../../ng-base/dist/base-core",
27
+    "marked": "^17.0.1",
29 28
     "prismjs": "^1.30.0",
30 29
     "rxjs": "~7.8.0",
30
+    "tabulator-tables": "^6.3.1",
31 31
     "tslib": "^2.3.0",
32 32
     "zone.js": "~0.15.0"
33 33
   },
@@ -37,6 +37,7 @@
37 37
     "@angular/compiler-cli": "^19.2.0",
38 38
     "@types/jasmine": "~5.1.0",
39 39
     "@types/prismjs": "^1.26.5",
40
+    "@types/tabulator-tables": "^6.3.1",
40 41
     "autoprefixer": "^10.4.23",
41 42
     "jasmine-core": "~5.6.0",
42 43
     "karma": "~6.4.0",

+ 2
- 2
src/app/app.component.ts Wyświetl plik

@@ -40,9 +40,9 @@ export class AppComponent implements OnInit, OnDestroy {
40 40
     console.log('AppComponent初始化');
41 41
     console.log('当前路由:', this.router.url);
42 42
     
43
-    // 启用真实API调用
43
+    // 禁用模拟数据,使用真实API
44 44
     this.config.useMockData = false;
45
-    console.log('使用真实API数据,配置:', {
45
+    console.log('使用真实API,配置:', {
46 46
       useMockData: this.config.useMockData,
47 47
       apiBaseUrl: this.config.apiBaseUrl
48 48
     });

+ 13
- 8
src/app/interceptors/mock.interceptor.ts Wyświetl plik

@@ -25,15 +25,20 @@ export const MockInterceptor: HttpInterceptorFn = (req, next) => {
25 25
   // 检查是否为公开端点(不需要认证)
26 26
   const isPublicEndpoint = req.url.includes('/login') || req.url.includes('/register');
27 27
   
28
-  // 为需要认证的API请求添加Basic认证头(暂时禁用)
28
+  // 为需要认证的API请求添加Basic认证头
29 29
   if (req.url.includes('/api/') && !isPublicEndpoint) {
30
-    console.log('API请求,添加默认认证头');
31
-    const defaultBasicAuth = 'YWRtaW46MTIz'; // base64("admin:123")
32
-    authReq = req.clone({
33
-      setHeaders: {
34
-        Authorization: `Basic ${defaultBasicAuth}`
35
-      }
36
-    });
30
+    const basicAuth = authService.getBasicAuth();
31
+    if (basicAuth) {
32
+      console.log('API请求,使用用户认证信息添加认证头');
33
+      authReq = req.clone({
34
+        setHeaders: {
35
+          Authorization: `Basic ${basicAuth}`
36
+        }
37
+      });
38
+    } else {
39
+      console.warn('API请求需要认证,但用户未登录,不添加认证头');
40
+      // 不添加认证头,后端会返回401错误,错误处理会跳转到登录页
41
+    }
37 42
   } else {
38 43
     console.log('公开端点或非API请求,不添加认证头');
39 44
   }

+ 124
- 0
src/app/models/config-meta.constants.ts Wyświetl plik

@@ -0,0 +1,124 @@
1
+/**
2
+ * 配置元信息相关常量定义
3
+ */
4
+
5
+// 字段类型枚举
6
+export const FIELD_TYPES = ['string', 'int', 'bool', 'config', 'array'] as const;
7
+export type FieldType = typeof FIELD_TYPES[number];
8
+
9
+// 配置名称常量
10
+export const CONFIG_NAMES = ['app', 'database', 'redis', 'logger', 'cache'] as const;
11
+
12
+// Tabulator字段映射:前端字段名 -> 后端字段名
13
+export const FIELD_MAPPING: Record<string, string> = {
14
+  'id': 'id',
15
+  'configName': 'configName',
16
+  'fieldName': 'fieldName',
17
+  'fieldType': 'fieldType',
18
+  'yamlName': 'yamlName',
19
+  'fieldDesc': 'fieldDesc',
20
+  'description': 'fieldDesc', // 兼容别名
21
+  'creator': 'creator',
22
+  'createdAt': 'createdAt'
23
+} as const;
24
+
25
+// Tabulator操作符映射:Tabulator操作符 -> 后端操作符
26
+export const OPERATOR_MAPPING: Record<string, string> = {
27
+  '=': '=',
28
+  '!=': '!=',
29
+  'like': 'like',
30
+  '>': '>',
31
+  '<': '<',
32
+  '>=': '>=',
33
+  '<=': '<=',
34
+  'in': 'in'
35
+} as const;
36
+
37
+// 调试信息默认值
38
+export const DEBUG_INFO_DEFAULTS = {
39
+  dataSource: '',
40
+  recordCount: 0,
41
+  lastUpdated: '',
42
+  useMockData: false,
43
+  registerUrl: '',
44
+  listUrl: '',
45
+  currentPage: 1,
46
+  totalPages: 1,
47
+  pageSize: 100  // 与组件中的paginationPageSize保持一致
48
+} as const;
49
+
50
+// Tabulator列配置工厂函数
51
+export function createTabulatorColumns() {
52
+  return [
53
+    { 
54
+      title: 'ID', 
55
+      field: 'id', 
56
+      width: 120, 
57
+      sorter: 'string',
58
+      headerFilter: true,
59
+      headerFilterPlaceholder: '筛选ID...'
60
+    },
61
+    { 
62
+      title: '配置名称', 
63
+      field: 'configName', 
64
+      width: 150, 
65
+      sorter: 'string',
66
+      headerFilter: true,
67
+      headerFilterFunc: 'like',
68
+      headerFilterPlaceholder: '筛选配置名称...'
69
+    },
70
+    { 
71
+      title: '字段名', 
72
+      field: 'fieldName', 
73
+      width: 150, 
74
+      sorter: 'string',
75
+      headerFilter: true,
76
+      headerFilterPlaceholder: '筛选字段名...'
77
+    },
78
+    { 
79
+      title: '字段类型', 
80
+      field: 'fieldType', 
81
+      width: 120, 
82
+      sorter: 'string',
83
+      headerFilter: 'select',
84
+      headerFilterParams: {
85
+        values: [...FIELD_TYPES],
86
+        clearable: true
87
+      },
88
+      headerFilterPlaceholder: '选择类型...'
89
+    },
90
+    { 
91
+      title: 'YAML名称', 
92
+      field: 'yamlName', 
93
+      width: 150, 
94
+      sorter: 'string',
95
+      headerFilter: true,
96
+      headerFilterFunc: 'like',
97
+      headerFilterPlaceholder: '筛选YAML名称...'
98
+    },
99
+    { 
100
+      title: '字段描述', 
101
+      field: 'fieldDesc', 
102
+      width: 200, 
103
+      sorter: 'string',
104
+      headerFilter: true,
105
+      headerFilterPlaceholder: '筛选描述...'
106
+    },
107
+    { 
108
+      title: '创建者', 
109
+      field: 'creator', 
110
+      width: 120, 
111
+      sorter: 'string',
112
+      headerFilter: true,
113
+      headerFilterPlaceholder: '筛选创建者...'
114
+    },
115
+    { 
116
+      title: '创建时间', 
117
+      field: 'createdAt', 
118
+      width: 180, 
119
+      sorter: 'date',
120
+      headerFilter: true,
121
+      headerFilterPlaceholder: 'YYYY-MM-DD...'
122
+    }
123
+  ];
124
+}

+ 52
- 2
src/app/models/config-meta.model.ts Wyświetl plik

@@ -9,27 +9,77 @@ export interface ConfigMeta {
9 9
   createdAt: string;
10 10
 }
11 11
 
12
+// 排序参数接口
13
+export interface SortParam {
14
+  field: string; // 前端字段名
15
+  order: string; // asc/desc
16
+}
17
+
18
+// 筛选参数接口
19
+export interface FilterParam {
20
+  field: string; // 前端字段名
21
+  operator: string; // 运算符枚举:=, !=, like, in, >, <, >=, <=
22
+  value: any; // 值(支持多种类型)
23
+}
24
+
12 25
 // 配置元信息查询请求参数(与后端ConfigMetaQueryRequest对应)
13 26
 export interface ConfigMetaQueryRequest {
14 27
   // 分页参数
15 28
   page?: number;      // 页码(从0开始)
16 29
   pageSize?: number;  // 每页大小
17 30
   
18
-  // 搜索参数
31
+  // 搜索参数(向后兼容)
19 32
   configName?: string; // 配置名称模糊搜索
20 33
   fieldName?: string;  // 字段名称模糊搜索
21 34
   yamlName?: string;   // YAML标签模糊搜索
22 35
   
23
-  // 排序参数
36
+  // 排序参数(向后兼容)
24 37
   sortField?: string; // 排序字段
25 38
   sortOrder?: string; // 排序方向: asc/desc
39
+  
40
+  // 通用查询参数(新)
41
+  sorts?: SortParam[];
42
+  filters?: FilterParam[];
26 43
 }
27 44
 
28 45
 // 分页查询响应接口
29 46
 export interface PaginatedQueryResult<T> {
30 47
   success?: boolean;
48
+  last_page?: number;
31 49
   data?: T;
32 50
   totalCount?: number;
33 51
   message?: string;
34 52
   error?: string;
53
+}
54
+
55
+// Tabulator 事件类型定义
56
+export interface TabulatorRowEvent {
57
+  row: {
58
+    getData: () => any;
59
+    getElement: () => HTMLElement;
60
+  };
61
+}
62
+
63
+export interface TabulatorDataLoadEvent {
64
+  data?: any[];
65
+  last_page?: number;
66
+  [key: string]: any;
67
+}
68
+
69
+export interface TabulatorSorter {
70
+  field: string;
71
+  dir: 'asc' | 'desc';
72
+}
73
+
74
+export interface TabulatorFilter {
75
+  field: string;
76
+  type: string;
77
+  value: any;
78
+}
79
+
80
+export interface TabulatorServerRequestEvent {
81
+  page: number;
82
+  size: number;
83
+  sorters: TabulatorSorter[];
84
+  filters: TabulatorFilter[];
35 85
 }

+ 4
- 38
src/app/pages/service-register-config/service-register-config.component.html Wyświetl plik

@@ -1,51 +1,17 @@
1
-<div class="h-full flex flex-col pt-0 px-2 pb-2 min-w-0">
2
-  <!-- 错误消息显示 -->
3
-  <div *ngIf="errorMessage" class="mb-2 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
4
-    {{ errorMessage }}
5
-  </div>
6
-  
7
-  <!-- 顶部标题区域 -->
1
+<!-- 顶部标题区域 -->
8 2
   <app-sticky-header
9 3
     [title]="title"
10 4
     [hintText]="hintText"
11 5
     [buttonText]="'注册'"
12 6
     [buttonIcon]="'add'"
13 7
     [buttonColor]="'primary'"
14
-    [disabled]="registering"
15
-    [loading]="registering"
16 8
     [showDebugInfo]="true"
17
-    [debugDataSource]="debugInfo.dataSource"
18
-    [debugRecordCount]="debugInfo.recordCount"
19
-    [debugLastUpdated]="debugInfo.lastUpdated"
20
-    [debugUseMockData]="debugInfo.useMockData"
21
-    [debugRegisterUrl]="debugInfo.registerUrl"
22
-    [debugListUrl]="debugInfo.listUrl"
23 9
     [autoDetect]="true"
24 10
     [scrollContainer]="'.content-area'"
25 11
     [scrollThreshold]="20"
26 12
     [compactThreshold]="80"
27
-    [widthTarget]="'#matCard'"
13
+    [widthTarget]="'#tabulator-table'"
28 14
     (buttonClick)="onRegister()"
29 15
   ></app-sticky-header>
30
-   
31
-  
32
-    <mat-card-content #matCard id="matCard" class="flex-1">
33
-        <!-- Tabulator表格组件 -->
34
-        <lib-tabulator-table
35
-          class="h-[800px]"
36
-          [columns]="tabulatorColumns"
37
-          [data]="tableData"
38
-          [pagination]="'local'"
39
-          [paginationSize]="paginationPageSize"
40
-          [paginationSizeSelector]="[10, 50, 100, 500]"
41
-          [selectable]="true"
42
-          [height]="'100%'"
43
-          [layout]="'fitColumns'"
44
-          [placeholder]="'暂无数据'"
45
-          (rowClick)="onRowClick($event)"
46
-          (rowSelected)="onRowSelected($event)"
47
-          #tabulator>
48
-        </lib-tabulator-table>
49
-    </mat-card-content>
50
-  
51
-</div>
16
+  <!-- Tabulator 容器 -->
17
+  <div id="tabulator-table" #matCard class="flex-1" ></div>

+ 20
- 40
src/app/pages/service-register-config/service-register-config.component.scss Wyświetl plik

@@ -1,47 +1,27 @@
1
-// 移除所有阴影和间隙,使sticky-header在滚动缩小后与浏览器无缝衔接
2
-::ng-deep app-sticky-header {
3
-  &.locked {
4
-    box-shadow: none !important;
5
-    background-color: transparent !important;
6
-    
7
-    .header-content {
8
-      box-shadow: none !important;
9
-      margin: 0 !important; // 移除5px间隙
10
-      border-top: none !important; // 移除顶部边框
11
-      border-radius: 0 !important; // 确保无圆角
12
-    }
13
-  }
1
+/* config-meta-tabulator.component.scss */
2
+.tabulator-container {
3
+  padding: 16px;
14 4
   
15
-  &.compact {
16
-    .header-content {
17
-      box-shadow: none !important;
18
-      border-top: none !important;
19
-      border-radius: 0 !important;
20
-    }
21
-  }
22
-}
23
-
24
-// 调整内容区域顶部间隙
25
-:host {
26
-  ::ng-deep .content-area > div {
27
-    padding-top: 0 !important; // 移除默认的5px顶部间隙
28
-  }
29
-  
30
-  // 确保sticky-header与内容无缝衔接
31
-  ::ng-deep app-sticky-header {
32
-    margin-bottom: 0 !important;
33
-    
34
-    .sticky-header {
35
-      top: 0 !important;
36
-    }
5
+  .toolbar {
6
+    margin-bottom: 16px;
37 7
     
38
-    &.locked {
39
-      top: 0 !important;
8
+    .btn {
9
+      margin-right: 8px;
10
+      padding: 8px 16px;
11
+      background: #007bff;
12
+      color: white;
13
+      border: none;
14
+      border-radius: 4px;
15
+      cursor: pointer;
40 16
       
41
-      .header-content {
42
-        border-top-width: 0 !important;
43
-        border-bottom: 1px solid #e2e8f0 !important; // 保留底部边框
17
+      &:hover {
18
+        background: #0056b3;
44 19
       }
45 20
     }
46 21
   }
22
+  
23
+  #tabulator-table {
24
+    border: 1px solid #ddd;
25
+    border-radius: 4px;
26
+  }
47 27
 }

+ 150
- 5
src/app/pages/service-register-config/service-register-config.component.spec.ts Wyświetl plik

@@ -1,16 +1,38 @@
1 1
 import { ComponentFixture, TestBed } from '@angular/core/testing';
2
-
3 2
 import { ServiceRegisterConfigComponent } from './service-register-config.component';
3
+import { ConfigMetaService } from '../../services/config-meta.service';
4
+import { MatSnackBar } from '@angular/material/snack-bar';
5
+import { ConfigService } from 'base-core';
6
+import { of } from 'rxjs';
4 7
 
5 8
 describe('ServiceRegisterConfigComponent', () => {
6 9
   let component: ServiceRegisterConfigComponent;
7 10
   let fixture: ComponentFixture<ServiceRegisterConfigComponent>;
11
+  let configMetaServiceSpy: jasmine.SpyObj<ConfigMetaService>;
12
+  let configServiceSpy: jasmine.SpyObj<ConfigService>;
13
+  let snackBarSpy: jasmine.SpyObj<MatSnackBar>;
8 14
 
9 15
   beforeEach(async () => {
16
+    configMetaServiceSpy = jasmine.createSpyObj('ConfigMetaService', [
17
+      'initConfigMeta',
18
+      'listConfigMetaPaginated',
19
+      'getInitUrl',
20
+      'getListUrl'
21
+    ]);
22
+    configServiceSpy = jasmine.createSpyObj('ConfigService', [], {
23
+      useMockData: false,
24
+      apiBaseUrl: 'http://localhost:8080'
25
+    });
26
+    snackBarSpy = jasmine.createSpyObj('MatSnackBar', ['open']);
27
+
10 28
     await TestBed.configureTestingModule({
11
-      imports: [ServiceRegisterConfigComponent]
12
-    })
13
-    .compileComponents();
29
+      imports: [ServiceRegisterConfigComponent],
30
+      providers: [
31
+        { provide: ConfigMetaService, useValue: configMetaServiceSpy },
32
+        { provide: ConfigService, useValue: configServiceSpy },
33
+        { provide: MatSnackBar, useValue: snackBarSpy }
34
+      ]
35
+    }).compileComponents();
14 36
 
15 37
     fixture = TestBed.createComponent(ServiceRegisterConfigComponent);
16 38
     component = fixture.componentInstance;
@@ -20,4 +42,127 @@ describe('ServiceRegisterConfigComponent', () => {
20 42
   it('should create', () => {
21 43
     expect(component).toBeTruthy();
22 44
   });
23
-});
45
+
46
+  it('should initialize with default values', () => {
47
+    expect(component.tabulatorColumns).toBeDefined();
48
+    expect(component.tabulatorDataSource).toBeDefined();
49
+    expect(component.isDataLoading).toBeFalse();
50
+    expect(component.dataCount).toBe(0);
51
+    expect(component.pageSize).toBe(20);
52
+  });
53
+
54
+  it('should call initConfigMeta and handle success response', () => {
55
+    const mockResponse = { success: true, data: '初始化成功' };
56
+    configMetaServiceSpy.initConfigMeta.and.returnValue(of(mockResponse));
57
+
58
+    component.initConfigMeta();
59
+
60
+    expect(configMetaServiceSpy.initConfigMeta).toHaveBeenCalled();
61
+    expect(component.isDataLoading).toBeFalse();
62
+    expect(snackBarSpy.open).toHaveBeenCalledWith('配置元信息初始化成功', '关闭', { duration: 3000 });
63
+  });
64
+
65
+  it('should call initConfigMeta and handle error response', () => {
66
+    const mockResponse = { success: false, message: '初始化失败' };
67
+    configMetaServiceSpy.initConfigMeta.and.returnValue(of(mockResponse));
68
+
69
+    component.initConfigMeta();
70
+
71
+    expect(configMetaServiceSpy.initConfigMeta).toHaveBeenCalled();
72
+    expect(snackBarSpy.open).toHaveBeenCalledWith('初始化失败: 初始化失败', '关闭', { duration: 5000 });
73
+  });
74
+
75
+  it('should call initConfigMeta and handle request error', () => {
76
+    const mockError = new Error('请求失败');
77
+    configMetaServiceSpy.initConfigMeta.and.throwError(mockError);
78
+
79
+    component.initConfigMeta();
80
+
81
+    expect(configMetaServiceSpy.initConfigMeta).toHaveBeenCalled();
82
+    expect(component.isDataLoading).toBeFalse();
83
+  });
84
+
85
+  it('should update debug info on init', () => {
86
+    configMetaServiceSpy.getInitUrl.and.returnValue('http://localhost:8080/init/config/meta');
87
+    configMetaServiceSpy.getListUrl.and.returnValue('http://localhost:8080/config/meta/list');
88
+
89
+    component.ngOnInit();
90
+
91
+    expect(component.debugInfo.registerUrl).toBe('http://localhost:8080/init/config/meta');
92
+    expect(component.debugInfo.listUrl).toBe('http://localhost:8080/config/meta/list');
93
+    expect(component.debugInfo.useMockData).toBeFalse();
94
+    expect(component.debugInfo.dataSource).toBe('真实 API');
95
+  });
96
+
97
+  it('should handle tabulator data request', () => {
98
+    const mockParams = {
99
+      page: 1,
100
+      size: 20,
101
+      sorters: [],
102
+      filters: []
103
+    };
104
+    const mockResponse = {
105
+      success: true,
106
+      data: [{ id: '1', configName: 'test' }],
107
+      totalCount: 1
108
+    };
109
+    configMetaServiceSpy.listConfigMetaPaginated.and.returnValue(of(mockResponse));
110
+
111
+    const result = component.onTabulatorDataRequest(mockParams);
112
+
113
+    expect(configMetaServiceSpy.listConfigMetaPaginated).toHaveBeenCalled();
114
+    expect(component.dataCount).toBe(1);
115
+    expect(component.totalCount).toBe(1);
116
+    expect(component.debugInfo.recordCount).toBe(1);
117
+  });
118
+
119
+  it('should handle tabulator data request with error', () => {
120
+    const mockParams = {
121
+      page: 1,
122
+      size: 20,
123
+      sorters: [],
124
+      filters: []
125
+    };
126
+    const mockError = new Error('请求失败');
127
+    configMetaServiceSpy.listConfigMetaPaginated.and.throwError(mockError);
128
+
129
+    const result = component.onTabulatorDataRequest(mockParams);
130
+
131
+    expect(configMetaServiceSpy.listConfigMetaPaginated).toHaveBeenCalled();
132
+    expect(component.isDataLoading).toBeFalse();
133
+    expect(snackBarSpy.open).toHaveBeenCalledWith('数据加载请求失败: ' + mockError.message, '关闭', { duration: 5000 });
134
+  });
135
+
136
+  it('should update debug info when page changes', () => {
137
+    const mockEvent = { page: 2, pageSize: 20 };
138
+    component.onPageChanged(mockEvent);
139
+
140
+    expect(component.currentPage).toBe(2);
141
+    expect(component.pageSize).toBe(20);
142
+    expect(component.debugInfo.currentPage).toBe(2);
143
+  });
144
+
145
+  it('should copy debug info to clipboard', () => {
146
+    spyOn(navigator.clipboard, 'writeText').and.returnValue(Promise.resolve());
147
+    component.debugInfo = {
148
+      dataSource: 'Mock 数据',
149
+      recordCount: 10,
150
+      lastUpdated: '2024-01-01 12:00:00',
151
+      useMockData: true,
152
+      registerUrl: 'http://localhost:8080/init',
153
+      listUrl: 'http://localhost:8080/list',
154
+      currentPage: 1,
155
+      totalPages: 1
156
+    };
157
+
158
+    component.copyDebugInfo();
159
+
160
+    expect(navigator.clipboard.writeText).toHaveBeenCalled();
161
+    expect(snackBarSpy.open).toHaveBeenCalledWith('调试信息已复制到剪贴板', '关闭', { duration: 2000 });
162
+  });
163
+
164
+  it('should refresh data and update debug info', () => {
165
+    component.refreshData();
166
+    expect(component.isDataLoading).toBeTrue();
167
+  });
168
+});

+ 229
- 288
src/app/pages/service-register-config/service-register-config.component.ts Wyświetl plik

@@ -1,336 +1,277 @@
1
-import { Component, OnInit, ChangeDetectorRef, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
2
-import { CommonModule } from '@angular/common';
3
-import { MatCard, MatCardContent } from '@angular/material/card';
4 1
 
5
-import { HttpErrorResponse } from '@angular/common/http';
6
-import { StickyHeaderComponent, TabulatorTableComponent } from 'base-core';
7
-import { ConfigMeta } from '../../models/config-meta.model';
2
+import { Component, AfterViewInit, inject } from '@angular/core';
8 3
 import { ConfigMetaService } from '../../services/config-meta.service';
9
-import { ConfigMetaQueryRequest, PaginatedQueryResult } from '../../models/config-meta.model';
4
+import { AuthService } from '../../services/auth.service';
5
+import { TabulatorMapper } from '../../utils/tabulator-mapper.util';
6
+import { ConfigService, StickyHeaderComponent } from 'base-core';
7
+import { CommonModule } from '@angular/common';
8
+
9
+// 声明全局类型
10
+declare var Tabulator: any;
11
+
10 12
 
11 13
 @Component({
14
+  imports: [CommonModule, StickyHeaderComponent],
12 15
   selector: 'app-service-register-config',
13
-  imports: [CommonModule, MatCard, MatCardContent, StickyHeaderComponent, TabulatorTableComponent],
14 16
   templateUrl: './service-register-config.component.html',
15 17
   styleUrl: './service-register-config.component.scss'
16 18
 })
17
-export class ServiceRegisterConfigComponent implements OnInit, AfterViewInit, OnDestroy {
19
+export class ServiceRegisterConfigComponent implements AfterViewInit {
20
+  private tabulator!: any;
21
+  private totalCount: number = 0;
22
+  private configMetaService = inject(ConfigMetaService);
23
+  private config = inject(ConfigService);
24
+  private authService = inject(AuthService);
25
+  
18 26
   title = '注册服务配置';
19 27
   hintText = '点击"注册"按钮将同步所有配置元信息到数据库,并显示配置列表。';
20
-   
21
-  // Tabulator 配置
22
-  tabulatorColumns: any[] = [
23
-    { title: 'ID', field: 'id', width: 120, sorter: 'string' },
24
-    { title: '配置名称', field: 'configName', width: 150, sorter: 'string' },
25
-    { title: '字段名', field: 'fieldName', width: 150, sorter: 'string' },
26
-    { title: '字段类型', field: 'fieldType', width: 120, sorter: 'string' },
27
-    { title: 'YAML名称', field: 'yamlName', width: 150, sorter: 'string' },
28
-    { title: '字段描述', field: 'fieldDesc', width: 200, sorter: 'string' },
29
-    { title: '创建者', field: 'creator', width: 120, sorter: 'string' },
30
-    { title: '创建时间', field: 'createdAt', width: 180, sorter: 'datetime' }
31
-  ];
32
-  
33
-  configMetaList: ConfigMeta[] = [];
34
-
35
-  // 分页配置
36
-  paginationPageSize = 100;
37
-  currentPage = 1;
38
-  totalRecords = 0;
39
-
40
-  // Tabulator 数据
41
-  tableData: any[] = [];
42
-
43
-  loading = false;
44 28
   registering = false;
45
-  errorMessage: string | null = null;
46
-  
47
-  // 注意:滚动状态现在由 sticky-header 组件自动检测管理
48
-  // 使用 [autoDetect]="true" 启用自动滚动检测
49
-
50
-  debugInfo = {
51
-    dataSource: '',
52
-    recordCount: 0,
53
-    lastUpdated: '',
54
-    useMockData: false,
55
-    registerUrl: '',
56
-    listUrl: '',
57
-    currentPage: 1,
58
-    totalPages: 1,
59
-    pageSize: 10
60
-  };
61
-
62
-  @ViewChild('tabulator') tabulator!: TabulatorTableComponent;
63
-  @ViewChild('matCard', { read: ElementRef }) matCardRef!: ElementRef;
64 29
 
65
-  constructor(
66
-    private configMetaService: ConfigMetaService,
67
-    private cdRef: ChangeDetectorRef,
68
-    private elementRef: ElementRef
69
-  ) {}
70
-
71
-  ngOnInit() {
72
-    console.log('ServiceRegisterConfigComponent initialized');
73
-    
74
-    // 默认使用API数据,可通过调试信息切换为模拟数据
75
-    this.debugInfo.useMockData = false;
76
-    this.updateDataConfig();
77
-    
78
-    // 加载数据
79
-    this.loadData();
80
-    
81
-    console.log('初始化完成,配置状态:', {
82
-      useMockData: this.debugInfo.useMockData,
83
-      tableDataLength: this.tableData.length,
84
-      paginationMode: 'local'
30
+  ngAfterViewInit(): void {
31
+    console.log('ServiceRegisterConfigComponent ngAfterViewInit');
32
+    console.log('Tabulator global available:', typeof Tabulator !== 'undefined');
33
+    console.log('ConfigService状态:', {
34
+      useMockData: this.config.useMockData,
35
+      apiBaseUrl: this.config.apiBaseUrl
85 36
     });
37
+    console.log('ConfigMetaService API URL:', this.configMetaService.getListUrl());
38
+    
39
+    // 使用 setTimeout 确保 DOM 已渲染
40
+    setTimeout(() => {
41
+      console.log('Initializing Tabulator...');
42
+      this.initTabulator();
43
+    }, 100);
86 44
   }
87
-
45
+  
88 46
   /**
89
-   * 加载数据(使用config-meta.service.ts)
47
+   * 初始化表格 - 标准远程分页版本
90 48
    */
91
-  loadData(page: number = 1): void {
92
-    console.log(`加载第${page}页数据,页大小: ${this.paginationPageSize}`);
49
+  private initTabulator(): void {
50
+    console.log('initTabulator called');
51
+    console.log('Table element exists:', document.getElementById('tabulator-table'));
93 52
     
94
-    // 构建查询请求
95
-    const request: ConfigMetaQueryRequest = {
96
-      page: page - 1, // API页码从0开始
97
-      pageSize: this.paginationPageSize
98
-    };
53
+    // 获取Basic认证信息
54
+    const basicAuth = this.authService.getBasicAuth();
55
+    console.log('Basic认证信息:', basicAuth ? '有' : '无');
99 56
     
100
-    this.loading = true;
101
-    
102
-    this.configMetaService.listConfigMetaPaginated(request).subscribe({
103
-       next: (result: PaginatedQueryResult<ConfigMeta[]>) => {
104
-         this.loading = false;
105
-         this.errorMessage = null;
106
-         
107
-         if (result.success && result.data) {
108
-          this.tableData = result.data;
109
-          this.configMetaList = result.data;
110
-          this.totalRecords = result.totalCount || result.data.length;
57
+    this.tabulator = new Tabulator('#tabulator-table', {
58
+     
59
+      // 列定义 - 使用现有列配置
60
+      columns: [
61
+        { 
62
+          title: 'ID', 
63
+          field: 'id', 
64
+          sorter: 'number', 
65
+          headerFilter: 'input',
66
+          width: 80
67
+        },
68
+        { 
69
+          title: '配置名称', 
70
+          field: 'configName', 
71
+          sorter: 'string', 
72
+          headerFilter: 'input',
73
+          headerFilterFunc: 'like'
74
+        },
75
+        { 
76
+          title: '字段名', 
77
+          field: 'fieldName', 
78
+          sorter: 'string', 
79
+          headerFilter: 'input',
80
+          headerFilterFunc: 'like'
81
+        },
82
+        { 
83
+          title: '字段类型', 
84
+          field: 'fieldType', 
85
+          sorter: 'string', 
86
+          headerFilter: 'input',
87
+          headerFilterParams: {
88
+            values: ['string', 'number', 'boolean', 'array', 'object']
89
+          }
90
+        },
91
+        { 
92
+          title: '描述', 
93
+          field: 'fieldDesc', 
94
+          sorter: 'string', 
95
+          headerFilter: 'input',
96
+          headerFilterFunc: 'like',
97
+          width: 300
98
+        }
99
+      ],
100
+      
101
+      // 标准远程分页配置
102
+      pagination: true,
103
+      paginationMode: 'remote',
104
+      paginationSize: 20,
105
+      paginationSizeSelector: [10, 20, 50, 100],
106
+      sortMode: 'remote',
107
+      filterMode: 'remote',
108
+      
109
+      // 使用标准AJAX配置,通过代理调用后端API
110
+      ajaxURL: '/api/config/meta/list',
111
+      ajaxConfig: {
112
+        method: 'POST',
113
+        headers: {
114
+          'Authorization': basicAuth ? `Basic ${basicAuth}` : '',
115
+          'Content-Type': 'application/json'
116
+        }
117
+      },
118
+      ajaxContentType: 'json',
119
+      
120
+      // // 参数生成器,将Tabulator参数转换为后端期望格式
121
+      // ajaxParamsGenerator: (url: string, config: any, params: any) => {
122
+      //   console.log('Tabulator AJAX参数生成器被调用', { url, config });
123
+      //   console.log('原始params类型:', typeof params);
124
+      //   console.log('原始params完整结构:', params);
125
+        
126
+      //   // 详细分析params对象
127
+      //   if (params) {
128
+      //     console.log('params所有键:', Object.keys(params));
129
+      //     console.log('page:', params.page);
130
+      //     console.log('size:', params.size);
131
+      //     console.log('sort类型:', typeof params.sort);
132
+      //     console.log('sort值:', params.sort);
133
+      //     console.log('filter类型:', typeof params.filter);
134
+      //     console.log('filter值:', params.filter);
111 135
           
112
-          // 更新调试信息
113
-          this.debugInfo.dataSource = 'API数据';
114
-          this.debugInfo.recordCount = this.totalRecords;
115
-          this.debugInfo.lastUpdated = new Date().toISOString();
116
-          this.debugInfo.useMockData = false;
117
-          this.debugInfo.registerUrl = this.configMetaService.getInitUrl();
118
-          this.debugInfo.listUrl = this.configMetaService.getListUrl();
119
-          this.debugInfo.currentPage = page;
120
-          this.debugInfo.totalPages = Math.ceil(this.totalRecords / this.paginationPageSize);
121
-          this.debugInfo.pageSize = this.paginationPageSize;
136
+      //     // 检查可能的别名
137
+      //     console.log('sorters存在?:', 'sorters' in params);
138
+      //     console.log('filters存在?:', 'filters' in params);
139
+      //   }
140
+        
141
+      //   // 处理params为undefined的情况
142
+      //   if (!params) {
143
+      //     params = {};
144
+      //   }
145
+        
146
+      //   // 构建请求参数,使用TabulatorMapper转换为后端期望格式
147
+      //   // Tabulator 6.x发送的是 sort/filter(单数),但后端期望 sorters/filters(复数)
148
+      //   const request = TabulatorMapper.buildServerRequest(
149
+      //     params.page || 1,
150
+      //     params.size || 20,
151
+      //     params.sorts || [],      // Tabulator使用单数'sort'
152
+      //     params.filters || []     // Tabulator使用单数'filter'
153
+      //   );
154
+        
155
+      //   console.log('生成的请求参数:', request);
156
+      //   return request;
157
+      // },
158
+      
159
+      // 响应处理,将后端响应转换为Tabulator期望格式
160
+      // ajaxResponse: (url: string, params: any, response: any) => {
161
+      //   console.log('Tabulator AJAX响应处理', { url, params, response });
162
+        
163
+      //   // 检查响应格式
164
+      //   if (response && typeof response === 'object') {
165
+      //     // 后端返回格式:{success: true, data: [...], totalCount: N, last_page: N}
166
+      //     const data = response.data || [];
167
+      //     const totalCount = response.totalCount || 0;
168
+      //     const pageSize = params.size || 20;
169
+      //     const lastPage = response.last_page || Math.ceil(totalCount / pageSize);
122 170
           
123
-          console.log(`数据加载成功: ${result.data.length}条记录,总计: ${this.totalRecords}`);
124
-          console.log('数据示例:', result.data.slice(0, 3)); // 打印前3条数据
125
-        } else {
126
-          console.error('数据加载失败:', result);
127
-          // 如果API失败,切换到模拟数据用于调试
128
-          this.debugInfo.useMockData = true;
129
-          this.updateDataConfig();
130
-        }
171
+      //     return {
172
+      //       data: data,
173
+      //       last_page: lastPage
174
+      //     };
175
+      //   }
131 176
         
132
-        this.cdRef.detectChanges();
133
-      },
134
-       error: (error: HttpErrorResponse) => {
135
-         this.loading = false;
136
-         console.error('加载数据时出错:', error);
137
-         console.error('错误详情:', error.error);
138
-         
139
-         // 设置错误消息
140
-         this.errorMessage = `加载数据失败: ${error.status} ${error.statusText}`;
141
-         
142
-         // API调用失败,切换到模拟数据
143
-         this.debugInfo.useMockData = true;
144
-         this.updateDataConfig();
145
-         
146
-         this.cdRef.detectChanges();
147
-       }
148
-    });
149
-  }
150
-
151
-  /**
152
-   * 生成模拟数据(用于调试)
153
-   */
154
-  private generateMockData(): ConfigMeta[] {
155
-    const mockData: ConfigMeta[] = [];
156
-    const configs = ['app', 'database', 'redis', 'logger', 'cache'];
157
-    const fieldTypes = ['string', 'int', 'bool', 'config', 'array'];
158
-    
159
-    for (let i = 1; i <= 200; i++) {
160
-      const configIndex = i % configs.length;
161
-      mockData.push({
162
-        id: `config-${i}`,
163
-        configName: configs[configIndex],
164
-        fieldName: `field_${i}`,
165
-        fieldType: fieldTypes[i % fieldTypes.length],
166
-        yamlName: `yaml.field.${i}`,
167
-        fieldDesc: `这是第 ${i} 个配置字段的描述`,
168
-        creator: `user_${(i % 5) + 1}`,
169
-        createdAt: new Date(Date.now() - i * 86400000).toISOString()
170
-      });
171
-    }
172
-    return mockData;
173
-  }
174
-
175
-  /**
176
-   * Tabulator事件处理器
177
-   */
178
-  
179
-  onDataLoading(event: any): void {
180
-    console.log('数据加载中:', event);
181
-    this.loading = true;
182
-    this.cdRef.detectChanges();
183
-  }
184
-
185
-  onDataLoaded(event: any): void {
186
-    console.log('数据加载完成:', event);
187
-    this.loading = false;
188
-    
189
-    // 提取实际数据
190
-    let data: any[] = [];
191
-    let totalCount = 0;
192
-    let lastPage = 0;
193
-    
194
-    if (this.debugInfo.useMockData) {
195
-      // 模拟数据模式下,event是数据数组
196
-      data = Array.isArray(event) ? event : [];
197
-      totalCount = data.length;
198
-      this.totalRecords = totalCount;
177
+      //   // 如果响应格式不符合预期,返回空数据
178
+      //   console.warn('响应格式不符合预期:', response);
179
+      //   return {
180
+      //     data: [],
181
+      //     last_page: 0
182
+      //   };
183
+      // },
199 184
       
200
-      // 更新调试信息
201
-      this.debugInfo.dataSource = '模拟数据';
202
-      this.debugInfo.recordCount = totalCount;
203
-      this.debugInfo.lastUpdated = new Date().toISOString();
204
-      this.debugInfo.registerUrl = '模拟数据 - 无API调用';
205
-      this.debugInfo.listUrl = '模拟数据 - 无API调用';
206
-      this.debugInfo.totalPages = Math.ceil(totalCount / this.paginationPageSize);
207
-    } else {
208
-      // 远程数据模式下,event是Tabulator格式 { last_page, data }
209
-      if (event && typeof event === 'object') {
210
-        if ('data' in event) {
211
-          data = Array.isArray(event.data) ? event.data : [];
185
+      tableAjaxError: (error: any) => {
186
+        console.error('Tabulator AJAX错误:', error);
187
+        if (error.status === 401 || error.status === 403) {
188
+          console.warn('认证失败,跳转到登录页面');
189
+          this.authService.logout();
212 190
         }
213
-        if ('last_page' in event) {
214
-          lastPage = event.last_page;
215
-          totalCount = lastPage * this.paginationPageSize;
216
-          this.totalRecords = totalCount;
217
-          this.debugInfo.totalPages = lastPage;
218
-        }
219
-      }
191
+      },
220 192
       
221
-      // 更新调试信息
222
-      this.debugInfo.dataSource = 'API数据';
223
-      this.debugInfo.recordCount = totalCount;
224
-      this.debugInfo.lastUpdated = new Date().toISOString();
225
-      this.debugInfo.useMockData = false;
226
-      this.debugInfo.registerUrl = this.configMetaService.getInitUrl();
227
-      this.debugInfo.listUrl = this.configMetaService.getListUrl();
228
-    }
229
-    
230
-    // 更新配置列表
231
-    this.configMetaList = data;
232
-    console.log(`数据加载完成: ${data.length}条记录,总计: ${totalCount}条`);
233
-    
234
-    this.cdRef.detectChanges();
235
-  }
236
-
237
-  onDataLoadError(error: any): void {
238
-    console.error('数据加载错误:', error);
239
-    this.loading = false;
240
-    this.cdRef.detectChanges();
241
-  }
242
-
243
-  onPageLoaded(page: number): void {
244
-    console.log('页面加载:', page);
245
-    this.currentPage = page;
246
-    this.debugInfo.currentPage = page;
247
-    this.cdRef.detectChanges();
248
-  }
249
-
250
-  onRowClick(event: any): void {
251
-    console.log('行点击:', event);
252
-  }
253
-
254
-  onRowSelected(event: any): void {
255
-    console.log('行选中:', event);
256
-  }
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-  ngAfterViewInit() {
269
-    console.log('ServiceRegisterConfigComponent view initialized');
270
-    // 注意:滚动检测现在由 sticky-header 组件自动处理
271
-    // 通过 [autoDetect]="true" 启用
272
-  }
273
-
274
-  /**
275
-   * 根据模拟数据状态更新数据配置
276
-   */
277
-  private updateDataConfig(): void {
278
-    console.log('更新数据配置,当前useMockData:', this.debugInfo.useMockData);
279
-    
280
-    if (this.debugInfo.useMockData) {
281
-      // 使用模拟数据,设置静态数据
282
-      this.tableData = this.generateMockData();
283
-      console.log('使用模拟数据:', {
284
-        dataLength: this.tableData.length,
285
-        paginationMode: 'local'
286
-      });
287
-    } else {
288
-      // 使用真实API,清空静态数据,将通过loadData()加载
289
-      this.tableData = [];
290
-      console.log('使用API数据:', {
291
-        dataLength: this.tableData.length,
292
-        paginationMode: 'remote'
293
-      });
294
-    }
295
-  }
296
-
297
-
298
-
299
-
300 193
 
301
-   ngOnDestroy() {
302
-    console.log('ServiceRegisterConfigComponent destroyed');
303
-    // 注意:滚动监听器现在由 sticky-header 组件自动管理
194
+      
195
+      // 表格样式
196
+      layout: 'fitColumns',
197
+      responsiveLayout: 'collapse',
198
+      height: '800px',
199
+      
200
+      // 初始加载
201
+      ajaxInitialLoad: true,
202
+      
203
+      // 分页位置
204
+      paginationCounter: 'rows',
205
+      paginationButtonCount: 5
206
+    });
304 207
   }
208
+  
305 209
 
306
-   onRegister() {
210
+  onRegister() {
307 211
      console.log('注册按钮被点击');
308 212
      this.registering = true;
309 213
      
310 214
      this.configMetaService.initConfigMeta().subscribe({
311 215
        next: (result: any) => {
312 216
          this.registering = false;
313
-         this.errorMessage = null;
217
+         //this.errorMessage = null;
314 218
          if (result.success) {
315 219
            console.log('注册完成:', result.data);
316 220
            
317 221
            // 注册后切换到真实API数据
318
-           this.debugInfo.useMockData = false;
319
-           this.updateDataConfig();
222
+           //this.debugInfo.useMockData = false;
223
+           //this.updateDataConfig();
320 224
            
321 225
            // 重新加载数据
322
-           this.loadData(1);
226
+           //this.loadData(1);
323 227
            
324 228
            console.log('注册成功,数据已重新加载');
325 229
          } else {
326 230
            console.error('注册失败:', result);
327 231
          }
328 232
        },
329
-       error: (error: HttpErrorResponse) => {
330
-         this.registering = false;
331
-         console.error('注册时出错:', error);
332
-         this.errorMessage = `注册失败: ${error.status} ${error.statusText}`;
333
-       }
233
+      
334 234
      });
335 235
    }
236
+  /**
237
+   * 刷新表格 - 重新加载数据
238
+   */
239
+  refresh(): void {
240
+    if (this.tabulator) {
241
+      this.tabulator.replaceData();
242
+    }
243
+  }
244
+  
245
+  /**
246
+   * 重置筛选和排序
247
+   */
248
+  reset(): void {
249
+    if (this.tabulator) {
250
+      this.tabulator.clearFilter();
251
+      this.tabulator.clearSort();
252
+    }
253
+  }
254
+
255
+  /**
256
+   * 刷新表格 - 模板别名
257
+   */
258
+  refreshTable(): void {
259
+    this.refresh();
260
+  }
261
+
262
+  /**
263
+   * 重置表格 - 模板别名
264
+   */
265
+  resetTable(): void {
266
+    this.reset();
267
+  }
268
+
269
+  /**
270
+   * 导出数据
271
+   */
272
+  exportData(): void {
273
+    if (this.tabulator) {
274
+      this.tabulator.download('csv', 'config-meta-data.csv');
275
+    }
276
+  }
336 277
 }

+ 9
- 69
src/app/services/config-meta.service.ts Wyświetl plik

@@ -3,6 +3,8 @@ import { HttpClient } from '@angular/common/http';
3 3
 import { Observable, of } from 'rxjs';
4 4
 import { map } from 'rxjs/operators';
5 5
 import { ConfigMeta, ConfigMetaQueryRequest, PaginatedQueryResult } from '../models/config-meta.model';
6
+import { CONFIG_NAMES, FIELD_TYPES } from '../models/config-meta.constants';
7
+import { MockDataUtil } from '../utils/mock-data.util';
6 8
 import { ConfigService } from 'base-core';
7 9
 import { QueryResult } from './auth.service';
8 10
 
@@ -11,7 +13,7 @@ import { QueryResult } from './auth.service';
11 13
 })
12 14
 export class ConfigMetaService {
13 15
   private initApiPath = '/init/config/meta';
14
-  private listApiPath = '/config/meta/list';
16
+  private listApiPath = '/tabulator/config/meta/list';
15 17
 
16 18
   constructor(
17 19
     private http: HttpClient,
@@ -115,73 +117,11 @@ export class ConfigMetaService {
115 117
       );
116 118
   }
117 119
 
118
-  private generateMockConfigMetaList(): ConfigMeta[] {
119
-    const mockList: ConfigMeta[] = [];
120
-    const configs = ['app', 'database', 'redis', 'logger', 'cache'];
121
-    const fieldTypes = ['string', 'int', 'bool', 'config', 'array'];
122
-    
123
-    for (let i = 1; i <= 20; i++) {
124
-      const configIndex = i % configs.length;
125
-      mockList.push({
126
-        id: `config-${i}`,
127
-        configName: configs[configIndex],
128
-        fieldName: `field_${i}`,
129
-        fieldType: fieldTypes[i % fieldTypes.length],
130
-        yamlName: `yaml.field.${i}`,
131
-        fieldDesc: `这是第 ${i} 个配置字段的描述`,
132
-        creator: `user_${(i % 5) + 1}`,
133
-        createdAt: new Date(Date.now() - i * 86400000).toISOString()
134
-      });
135
-    }
136
-    return mockList;
137
-  }
120
+   private generateMockConfigMetaList(): ConfigMeta[] {
121
+     return MockDataUtil.generateConfigMetaList(20);
122
+   }
138 123
 
139
-  private generateMockPaginatedData(request: ConfigMetaQueryRequest): PaginatedQueryResult<ConfigMeta[]> {
140
-    // 生成完整的模拟数据
141
-    const allData = this.generateMockConfigMetaList();
142
-    
143
-    // 应用过滤条件
144
-    let filteredData = allData.filter(item => {
145
-      if (request.configName && !item.configName.toLowerCase().includes(request.configName.toLowerCase())) {
146
-        return false;
147
-      }
148
-      if (request.fieldName && !item.fieldName.toLowerCase().includes(request.fieldName.toLowerCase())) {
149
-        return false;
150
-      }
151
-      if (request.yamlName && !item.yamlName.toLowerCase().includes(request.yamlName.toLowerCase())) {
152
-        return false;
153
-      }
154
-      return true;
155
-    });
156
-    
157
-    // 应用排序
158
-    if (request.sortField) {
159
-      const sortField = request.sortField as keyof ConfigMeta;
160
-      const sortOrder = request.sortOrder === 'desc' ? -1 : 1;
161
-      
162
-      filteredData.sort((a, b) => {
163
-        const aVal = a[sortField] || '';
164
-        const bVal = b[sortField] || '';
165
-        
166
-        if (aVal < bVal) return -1 * sortOrder;
167
-        if (aVal > bVal) return 1 * sortOrder;
168
-        return 0;
169
-      });
170
-    }
171
-    
172
-    const totalCount = filteredData.length;
173
-    
174
-    // 应用分页
175
-    const page = request.page || 0;
176
-    const pageSize = request.pageSize || 10;
177
-    const startIndex = page * pageSize;
178
-    const endIndex = startIndex + pageSize;
179
-    const pagedData = filteredData.slice(startIndex, endIndex);
180
-    
181
-    return {
182
-      success: true,
183
-      data: pagedData,
184
-      totalCount: totalCount
185
-    };
186
-  }
124
+   private generateMockPaginatedData(request: ConfigMetaQueryRequest): PaginatedQueryResult<ConfigMeta[]> {
125
+     return MockDataUtil.generateMockPaginatedData(request, 100);
126
+   }
187 127
 }

+ 94
- 0
src/app/services/debug-info.service.ts Wyświetl plik

@@ -0,0 +1,94 @@
1
+import { Injectable } from '@angular/core';
2
+import { DEBUG_INFO_DEFAULTS } from '../models/config-meta.constants';
3
+
4
+export interface DebugInfo {
5
+  dataSource: string;
6
+  recordCount: number;
7
+  lastUpdated: string;
8
+  useMockData: boolean;
9
+  registerUrl: string;
10
+  listUrl: string;
11
+  currentPage: number;
12
+  totalPages: number;
13
+  pageSize: number;
14
+}
15
+
16
+@Injectable({
17
+  providedIn: 'root'
18
+})
19
+export class DebugInfoService {
20
+  private debugInfo: DebugInfo = { ...DEBUG_INFO_DEFAULTS };
21
+
22
+  /**
23
+   * 获取当前调试信息
24
+   */
25
+  getDebugInfo(): DebugInfo {
26
+    return { ...this.debugInfo };
27
+  }
28
+
29
+  /**
30
+   * 更新调试信息(部分更新)
31
+   */
32
+  updateDebugInfo(updates: Partial<DebugInfo>): void {
33
+    this.debugInfo = { ...this.debugInfo, ...updates };
34
+  }
35
+
36
+  /**
37
+   * 重置为默认值
38
+   */
39
+  reset(): void {
40
+    this.debugInfo = { ...DEBUG_INFO_DEFAULTS };
41
+  }
42
+
43
+  /**
44
+   * 更新为远程API数据模式
45
+   */
46
+  updateForRemoteData(
47
+    recordCount: number,
48
+    currentPage: number,
49
+    totalPages: number,
50
+    pageSize: number,
51
+    registerUrl: string,
52
+    listUrl: string
53
+  ): void {
54
+    this.updateDebugInfo({
55
+      dataSource: 'API数据',
56
+      recordCount,
57
+      lastUpdated: new Date().toISOString(),
58
+      useMockData: false,
59
+      registerUrl,
60
+      listUrl,
61
+      currentPage,
62
+      totalPages,
63
+      pageSize
64
+    });
65
+  }
66
+
67
+  /**
68
+   * 更新为模拟数据模式
69
+   */
70
+  updateForMockData(recordCount: number, totalPages: number, pageSize: number): void {
71
+    this.updateDebugInfo({
72
+      dataSource: '模拟数据',
73
+      recordCount,
74
+      lastUpdated: new Date().toISOString(),
75
+      useMockData: true,
76
+      registerUrl: '模拟数据 - 无API调用',
77
+      listUrl: '模拟数据 - 无API调用',
78
+      totalPages,
79
+      pageSize
80
+    });
81
+  }
82
+
83
+  /**
84
+   * 更新分页信息
85
+   */
86
+  updatePagination(currentPage: number, totalRecords: number, pageSize: number): void {
87
+    const totalPages = Math.ceil(totalRecords / pageSize);
88
+    this.updateDebugInfo({
89
+      currentPage,
90
+      totalPages,
91
+      pageSize
92
+    });
93
+  }
94
+}

+ 220
- 0
src/app/services/mock-config-meta.service.ts Wyświetl plik

@@ -0,0 +1,220 @@
1
+// config-meta.mock.data.ts
2
+// 完全独立的模拟数据文件,不依赖任何其他模块
3
+
4
+/**
5
+ * 配置元数据结构定义
6
+ */
7
+export interface MockConfigMeta {
8
+  id: number;
9
+  configName: string;
10
+  fieldName: string;
11
+  yamlName: string;
12
+  fieldType: string;
13
+  description: string;
14
+  defaultValue?: string;
15
+  isRequired?: boolean;
16
+  createdAt?: Date;
17
+  updatedAt?: Date;
18
+}
19
+
20
+/**
21
+ * 分页查询请求参数
22
+ */
23
+export interface MockQueryRequest {
24
+  page?: number;
25
+  pageSize?: number;
26
+  sort?: Array<{field: string; order: 'ASC' | 'DESC'}>;
27
+  filter?: {[key: string]: {type: string; value: string; operator?: string}};
28
+}
29
+
30
+/**
31
+ * 分页查询结果
32
+ */
33
+export interface MockPaginatedResult {
34
+  success: boolean;
35
+  data: MockConfigMeta[];
36
+  totalCount: number;
37
+  page: number;
38
+  pageSize: number;
39
+  totalPages: number;
40
+  message?: string;
41
+}
42
+
43
+/**
44
+ * 模拟数据生成器
45
+ */
46
+export class MockDataGenerator {
47
+  // 静态模拟数据 - 直接硬编码
48
+  private static mockData: MockConfigMeta[] = [];
49
+
50
+  // 初始化模拟数据
51
+  static initialize() {
52
+    if (this.mockData.length > 0) return;
53
+    
54
+    const configNames = ['用户配置', '系统配置', '数据库配置', '缓存配置', '安全配置', '日志配置'];
55
+    const fieldTypes = ['string', 'number', 'boolean', 'array', 'object'];
56
+    
57
+    // 生成100条模拟数据
58
+    for (let i = 1; i <= 100; i++) {
59
+      const configIdx = Math.floor(Math.random() * configNames.length);
60
+      const typeIdx = Math.floor(Math.random() * fieldTypes.length);
61
+      
62
+      this.mockData.push({
63
+        id: i,
64
+        configName: configNames[configIdx],
65
+        fieldName: `field_${this.generateRandomName()}_${i}`,
66
+        yamlName: `config.${configNames[configIdx].replace('配置', '').toLowerCase()}.field_${i}`,
67
+        fieldType: fieldTypes[typeIdx],
68
+        description: `这是${configNames[configIdx]}的第${i}个字段,类型为${fieldTypes[typeIdx]}`,
69
+        defaultValue: i % 3 === 0 ? 'default_value' : undefined,
70
+        isRequired: i % 2 === 0,
71
+        createdAt: new Date(),
72
+        updatedAt: new Date()
73
+      });
74
+    }
75
+    
76
+    console.log(`✅ 生成 ${this.mockData.length} 条模拟数据`);
77
+  }
78
+
79
+  // 生成随机名称
80
+  private static generateRandomName(): string {
81
+    const prefixes = ['user', 'sys', 'db', 'cache', 'log', 'auth', 'api', 'web'];
82
+    const suffixes = ['name', 'id', 'type', 'status', 'time', 'date', 'count', 'size'];
83
+    return `${prefixes[Math.floor(Math.random() * prefixes.length)]}_${suffixes[Math.floor(Math.random() * suffixes.length)]}`;
84
+  }
85
+
86
+  /**
87
+   * 获取分页数据
88
+   */
89
+  static getPaginatedData(request: MockQueryRequest): Promise<MockPaginatedResult> {
90
+    this.initialize();
91
+    
92
+    // 深拷贝数据以避免修改原数据
93
+    let filteredData = [...this.mockData];
94
+    
95
+    console.log('📊 模拟请求参数:', request);
96
+    
97
+    // 1. 应用筛选
98
+    if (request.filter) {
99
+      Object.keys(request.filter).forEach(field => {
100
+        const filter = request.filter![field];
101
+        if (filter && filter.value !== undefined && filter.value !== '') {
102
+          filteredData = filteredData.filter(item => {
103
+            const fieldValue = String((item as any)[field] || '').toLowerCase();
104
+            const filterValue = String(filter.value || '').toLowerCase();
105
+            
106
+            if (filter.operator === 'contains' || filter.type === 'like') {
107
+              return fieldValue.includes(filterValue);
108
+            } else if (filter.operator === 'equals' || filter.type === '=') {
109
+              return fieldValue === filterValue;
110
+            }
111
+            return true;
112
+          });
113
+        }
114
+      });
115
+    }
116
+    
117
+    // 2. 应用排序
118
+    if (request.sort && request.sort.length > 0) {
119
+      filteredData.sort((a, b) => {
120
+        for (const sortItem of request.sort!) {
121
+          const aValue = (a as any)[sortItem.field];
122
+          const bValue = (b as any)[sortItem.field];
123
+          
124
+          if (aValue !== bValue) {
125
+            const comparison = String(aValue).localeCompare(String(bValue));
126
+            return sortItem.order === 'ASC' ? comparison : -comparison;
127
+          }
128
+        }
129
+        return 0;
130
+      });
131
+    }
132
+    
133
+    // 3. 分页
134
+    const page = request.page || 1;
135
+    const pageSize = request.pageSize || 20;
136
+    const startIndex = (page - 1) * pageSize;
137
+    const endIndex = Math.min(startIndex + pageSize, filteredData.length);
138
+    const pagedData = filteredData.slice(startIndex, endIndex);
139
+    
140
+    // 构建结果
141
+    const result: MockPaginatedResult = {
142
+      success: true,
143
+      data: pagedData,
144
+      totalCount: filteredData.length,
145
+      page: page,
146
+      pageSize: pageSize,
147
+      totalPages: Math.ceil(filteredData.length / pageSize),
148
+      message: '模拟数据加载成功'
149
+    };
150
+    
151
+    console.log(`📈 模拟响应: 第${page}页,共${result.totalPages}页,${pagedData.length}条数据,总计${filteredData.length}条`);
152
+    
153
+    // 模拟网络延迟
154
+    return new Promise(resolve => {
155
+      setTimeout(() => {
156
+        resolve(result);
157
+      }, 300);
158
+    });
159
+  }
160
+
161
+  /**
162
+   * 获取所有数据(不分页)
163
+   */
164
+  static getAllData(): MockConfigMeta[] {
165
+    this.initialize();
166
+    return [...this.mockData];
167
+  }
168
+
169
+  /**
170
+   * 根据ID获取单条数据
171
+   */
172
+  static getById(id: number): MockConfigMeta | undefined {
173
+    this.initialize();
174
+    return this.mockData.find(item => item.id === id);
175
+  }
176
+
177
+  /**
178
+   * 搜索数据
179
+   */
180
+  static search(keyword: string): MockConfigMeta[] {
181
+    this.initialize();
182
+    if (!keyword.trim()) return [...this.mockData];
183
+    
184
+    const lowerKeyword = keyword.toLowerCase();
185
+    return this.mockData.filter(item =>
186
+      item.configName.toLowerCase().includes(lowerKeyword) ||
187
+      item.fieldName.toLowerCase().includes(lowerKeyword) ||
188
+      item.description.toLowerCase().includes(lowerKeyword)
189
+    );
190
+  }
191
+
192
+  /**
193
+   * 获取统计数据
194
+   */
195
+  static getStats() {
196
+    this.initialize();
197
+    const stats = {
198
+      total: this.mockData.length,
199
+      byConfigName: {} as {[key: string]: number},
200
+      byFieldType: {} as {[key: string]: number}
201
+    };
202
+    
203
+    this.mockData.forEach(item => {
204
+      stats.byConfigName[item.configName] = (stats.byConfigName[item.configName] || 0) + 1;
205
+      stats.byFieldType[item.fieldType] = (stats.byFieldType[item.fieldType] || 0) + 1;
206
+    });
207
+    
208
+    return stats;
209
+  }
210
+}
211
+
212
+// 导出类型和类
213
+export type {
214
+  MockConfigMeta as ConfigMeta,
215
+  MockQueryRequest as ConfigMetaQueryRequest,
216
+  MockPaginatedResult as PaginatedQueryResult
217
+};
218
+
219
+// 默认导出数据生成器
220
+export default MockDataGenerator;

+ 131
- 0
src/app/utils/mock-data.util.ts Wyświetl plik

@@ -0,0 +1,131 @@
1
+/**
2
+ * 模拟数据生成工具类
3
+ * 为配置元信息生成一致的模拟数据
4
+ */
5
+import { ConfigMeta, ConfigMetaQueryRequest, PaginatedQueryResult } from '../models/config-meta.model';
6
+import { CONFIG_NAMES, FIELD_TYPES } from '../models/config-meta.constants';
7
+
8
+export class MockDataUtil {
9
+  /**
10
+   * 生成配置元信息模拟数据
11
+   * @param count 生成数量(默认20)
12
+   */
13
+  static generateConfigMetaList(count: number = 20): ConfigMeta[] {
14
+    const mockList: ConfigMeta[] = [];
15
+    const configs = [...CONFIG_NAMES];
16
+    const fieldTypes = [...FIELD_TYPES];
17
+    
18
+    for (let i = 1; i <= count; i++) {
19
+      const configIndex = i % configs.length;
20
+      mockList.push({
21
+        id: `config-${i}`,
22
+        configName: configs[configIndex],
23
+        fieldName: `field_${i}`,
24
+        fieldType: fieldTypes[i % fieldTypes.length],
25
+        yamlName: `yaml.field.${i}`,
26
+        fieldDesc: `这是第 ${i} 个配置字段的描述`,
27
+        creator: `user_${(i % 5) + 1}`,
28
+        createdAt: new Date(Date.now() - i * 86400000).toISOString()
29
+      });
30
+    }
31
+    return mockList;
32
+  }
33
+
34
+  /**
35
+   * 生成分页模拟数据(支持过滤和排序)
36
+   */
37
+  static generateMockPaginatedData(
38
+    request: ConfigMetaQueryRequest,
39
+    totalCount: number = 100
40
+  ): PaginatedQueryResult<ConfigMeta[]> {
41
+    // 生成完整数据集
42
+    const allData = this.generateConfigMetaList(totalCount);
43
+    
44
+    // 应用过滤条件 - 支持旧字段和新filters数组
45
+    let filteredData = allData.filter(item => {
46
+      // 旧字段过滤(向后兼容)
47
+      if (request.configName && !item.configName.toLowerCase().includes(request.configName.toLowerCase())) {
48
+        return false;
49
+      }
50
+      if (request.fieldName && !item.fieldName.toLowerCase().includes(request.fieldName.toLowerCase())) {
51
+        return false;
52
+      }
53
+      if (request.yamlName && !item.yamlName.toLowerCase().includes(request.yamlName.toLowerCase())) {
54
+        return false;
55
+      }
56
+      
57
+      // 新filters数组过滤(简单实现)
58
+      if (request.filters && request.filters.length > 0) {
59
+        for (const filter of request.filters) {
60
+          const fieldValue = item[filter.field as keyof ConfigMeta];
61
+          if (fieldValue === undefined) continue;
62
+          
63
+          const fieldStr = String(fieldValue).toLowerCase();
64
+          const filterStr = String(filter.value).toLowerCase();
65
+          
66
+          switch (filter.operator) {
67
+            case '=':
68
+              if (fieldStr !== filterStr) return false;
69
+              break;
70
+            case '!=':
71
+              if (fieldStr === filterStr) return false;
72
+              break;
73
+            case 'like':
74
+              if (!fieldStr.includes(filterStr)) return false;
75
+              break;
76
+            default:
77
+              // 其他操作符暂不实现
78
+              break;
79
+          }
80
+        }
81
+      }
82
+      return true;
83
+    });
84
+    
85
+    // 应用排序 - 支持旧字段和新sorts数组
86
+    if (request.sorts && request.sorts.length > 0) {
87
+      // 使用新sorts数组
88
+      filteredData.sort((a, b) => {
89
+        for (const sort of request.sorts!) {
90
+          const field = sort.field as keyof ConfigMeta;
91
+          const aVal = a[field] || '';
92
+          const bVal = b[field] || '';
93
+          const sortOrder = sort.order === 'desc' ? -1 : 1;
94
+          
95
+          if (aVal < bVal) return -1 * sortOrder;
96
+          if (aVal > bVal) return 1 * sortOrder;
97
+        }
98
+        return 0;
99
+      });
100
+    } else if (request.sortField) {
101
+      // 旧字段排序(向后兼容)
102
+      const sortField = request.sortField as keyof ConfigMeta;
103
+      const sortOrder = request.sortOrder === 'desc' ? -1 : 1;
104
+      
105
+      filteredData.sort((a, b) => {
106
+        const aVal = a[sortField] || '';
107
+        const bVal = b[sortField] || '';
108
+        
109
+        if (aVal < bVal) return -1 * sortOrder;
110
+        if (aVal > bVal) return 1 * sortOrder;
111
+        return 0;
112
+      });
113
+    }
114
+    
115
+    const finalCount = filteredData.length;
116
+    
117
+    // 应用分页
118
+    const page = request.page || 0;
119
+    const pageSize = request.pageSize || 10;
120
+    const startIndex = page * pageSize;
121
+    const endIndex = startIndex + pageSize;
122
+    const pagedData = filteredData.slice(startIndex, endIndex);
123
+    
124
+    return {
125
+      success: true,
126
+      data: pagedData,
127
+      totalCount: finalCount,
128
+      message: '模拟数据'
129
+    };
130
+  }
131
+}

+ 68
- 0
src/app/utils/tabulator-mapper.util.ts Wyświetl plik

@@ -0,0 +1,68 @@
1
+/**
2
+ * Tabulator映射工具类
3
+ * 处理前端字段名、操作符到后端格式的转换
4
+ */
5
+import { FIELD_MAPPING, OPERATOR_MAPPING } from '../models/config-meta.constants';
6
+import { TabulatorSorter, TabulatorFilter, ConfigMetaQueryRequest } from '../models/config-meta.model';
7
+
8
+export class TabulatorMapper {
9
+  /**
10
+   * 字段名映射:前端字段名 -> 后端字段名
11
+   */
12
+  static mapFieldName(field: string): string {
13
+    return FIELD_MAPPING[field] || field;
14
+  }
15
+
16
+  /**
17
+   * 操作符映射:Tabulator操作符 -> 后端操作符
18
+   */
19
+  static mapOperator(type: string): string {
20
+    return OPERATOR_MAPPING[type] || '=';
21
+  }
22
+
23
+   /**
24
+    * 将Tabulator排序器数组转换为后端排序参数数组
25
+    */
26
+   static mapSorters(sorters: TabulatorSorter[]): Array<{ field: string; order: string }> {
27
+     if (!sorters || sorters.length === 0) {
28
+       return [];
29
+     }
30
+ 
31
+     return sorters.map(sorter => ({
32
+       field: this.mapFieldName(sorter.field),
33
+       order: sorter.dir || 'asc'
34
+     }));
35
+   }
36
+ 
37
+   /**
38
+    * 将Tabulator筛选器数组转换为后端筛选参数数组
39
+    */
40
+   static mapFilters(filters: TabulatorFilter[]): Array<{ field: string; operator: string; value: any }> {
41
+     if (!filters || filters.length === 0) {
42
+       return [];
43
+     }
44
+ 
45
+     return filters.map(filter => ({
46
+       field: this.mapFieldName(filter.field),
47
+       operator: this.mapOperator(filter.type),
48
+       value: filter.value
49
+     }));
50
+   }
51
+ 
52
+   /**
53
+    * 构建服务端请求参数
54
+    */
55
+   static buildServerRequest(
56
+     page: number,
57
+     pageSize: number,
58
+     sorters: TabulatorSorter[] = [],
59
+     filters: TabulatorFilter[] = []
60
+   ): ConfigMetaQueryRequest {
61
+     return {
62
+       page: page - 1, // 后端从0开始
63
+       pageSize,
64
+       sorts: this.mapSorters(sorters),
65
+       filters: this.mapFilters(filters)
66
+     };
67
+   }
68
+}

Ładowanie…
Anuluj
Zapisz