Selaa lähdekoodia

支持分页了

qdy 1 kuukausi sitten
commit
7dcb9e7096
33 muutettua tiedostoa jossa 1644 lisäystä ja 368 poistoa
  1. 2
    1
      angular.json
  2. 37
    0
      package-lock.json
  3. 3
    0
      package.json
  4. 20
    0
      src/app/models/config-meta.model.ts
  5. 17
    17
      src/app/pages/service-register-config/service-register-config.component.html
  6. 223
    348
      src/app/pages/service-register-config/service-register-config.component.ts
  7. 16
    0
      src/app/services/agent.service.spec.ts
  8. 9
    0
      src/app/services/agent.service.ts
  9. 23
    0
      src/app/services/auth.service.spec.ts
  10. 199
    0
      src/app/services/auth.service.ts
  11. 187
    0
      src/app/services/config-meta.service.ts
  12. 16
    0
      src/app/services/config.service.spec.ts
  13. 25
    0
      src/app/services/config.service.ts
  14. 16
    0
      src/app/services/markdown.service.spec.ts
  15. 110
    0
      src/app/services/markdown.service.ts
  16. 16
    0
      src/app/services/project.service.spec.ts
  17. 78
    0
      src/app/services/project.service.ts
  18. 16
    0
      src/app/services/role.service.spec.ts
  19. 78
    0
      src/app/services/role.service.ts
  20. 16
    0
      src/app/services/service-management.service.spec.ts
  21. 9
    0
      src/app/services/service-management.service.ts
  22. 16
    0
      src/app/services/skill.service.spec.ts
  23. 9
    0
      src/app/services/skill.service.ts
  24. 16
    0
      src/app/services/tenant.service.spec.ts
  25. 76
    0
      src/app/services/tenant.service.ts
  26. 101
    0
      src/app/services/toast.service.ts
  27. 16
    0
      src/app/services/tree.service.spec.ts
  28. 180
    0
      src/app/services/tree.service.ts
  29. 16
    0
      src/app/services/user.service.spec.ts
  30. 80
    0
      src/app/services/user.service.ts
  31. 4
    0
      src/styles.scss
  32. 2
    2
      tsconfig.json
  33. 12
    0
      vite.config.ts

+ 2
- 1
angular.json Näytä tiedosto

@@ -38,7 +38,8 @@
38 38
             "styles": [
39 39
               "src/styles.scss"
40 40
             ],
41
-            "scripts": []
41
+            "scripts": [],
42
+            "allowedCommonJsDependencies": ["ag-grid-angular", "ag-grid-community"]
42 43
           },
43 44
           "configurations": {
44 45
             "production": {

+ 37
- 0
package-lock.json Näytä tiedosto

@@ -19,7 +19,10 @@
19 19
         "@angular/platform-browser-dynamic": "^19.2.0",
20 20
         "@angular/router": "^19.2.0",
21 21
         "@types/marked": "^5.0.2",
22
+        "ag-grid-angular": "^35.0.0",
23
+        "ag-grid-community": "^35.0.0",
22 24
         "marked": "^17.0.1",
25
+        "ng-base": "file:../../ng-base/dist/ng-base",
23 26
         "prismjs": "^1.30.0",
24 27
         "rxjs": "~7.8.0",
25 28
         "tslib": "^2.3.0",
@@ -43,6 +46,7 @@
43 46
         "typescript": "~5.7.2"
44 47
       }
45 48
     },
49
+    "../../ng-base/dist/ng-base": {},
46 50
     "node_modules/@alloc/quick-lru": {
47 51
       "version": "5.2.0",
48 52
       "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -6149,6 +6153,35 @@
6149 6153
         "node": ">=8.9.0"
6150 6154
       }
6151 6155
     },
6156
+    "node_modules/ag-charts-types": {
6157
+      "version": "13.0.1",
6158
+      "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-13.0.1.tgz",
6159
+      "integrity": "sha512-qg9adyiAaeUaDtWZEEPF45dv55kdJTe6Ghi0EQXCS79h/7KvOd6dxhqGZjPL1zeFl/L9qEXuYgb+LkGStq4mgQ==",
6160
+      "license": "MIT"
6161
+    },
6162
+    "node_modules/ag-grid-angular": {
6163
+      "version": "35.0.1",
6164
+      "resolved": "https://registry.npmjs.org/ag-grid-angular/-/ag-grid-angular-35.0.1.tgz",
6165
+      "integrity": "sha512-+Y1K/ZxiEbd3GhZcRWzLf60yiP4ECNoTCCqI65EgHiiRGInz3g1pDueArR7hE7V+z5gluA3NMGB3VrOZYUBKLg==",
6166
+      "license": "MIT",
6167
+      "dependencies": {
6168
+        "ag-grid-community": "35.0.1",
6169
+        "tslib": "^2.3.0"
6170
+      },
6171
+      "peerDependencies": {
6172
+        "@angular/common": ">= 18.0.0",
6173
+        "@angular/core": ">= 18.0.0"
6174
+      }
6175
+    },
6176
+    "node_modules/ag-grid-community": {
6177
+      "version": "35.0.1",
6178
+      "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-35.0.1.tgz",
6179
+      "integrity": "sha512-fYYZdymWKsN9/tZv+R6uZRnuiWYEQr+GHl85ZrB0ixbFcE8opYK4NJI29NnMc9ShYiCBnAO9hj54IFa+FI4aDA==",
6180
+      "license": "MIT",
6181
+      "dependencies": {
6182
+        "ag-charts-types": "13.0.1"
6183
+      }
6184
+    },
6152 6185
     "node_modules/agent-base": {
6153 6186
       "version": "7.1.4",
6154 6187
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -11189,6 +11222,10 @@
11189 11222
       "dev": true,
11190 11223
       "license": "MIT"
11191 11224
     },
11225
+    "node_modules/ng-base": {
11226
+      "resolved": "../../ng-base/dist/ng-base",
11227
+      "link": true
11228
+    },
11192 11229
     "node_modules/node-addon-api": {
11193 11230
       "version": "6.1.0",
11194 11231
       "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",

+ 3
- 0
package.json Näytä tiedosto

@@ -21,7 +21,10 @@
21 21
     "@angular/platform-browser-dynamic": "^19.2.0",
22 22
     "@angular/router": "^19.2.0",
23 23
     "@types/marked": "^5.0.2",
24
+    "ag-grid-angular": "^35.0.0",
25
+    "ag-grid-community": "^35.0.0",
24 26
     "marked": "^17.0.1",
27
+    "ng-base": "file:../../ng-base/dist/ng-base",
25 28
     "prismjs": "^1.30.0",
26 29
     "rxjs": "~7.8.0",
27 30
     "tslib": "^2.3.0",

+ 20
- 0
src/app/models/config-meta.model.ts Näytä tiedosto

@@ -0,0 +1,20 @@
1
+import { ConfigMeta } from 'ng-base';
2
+
3
+export interface ConfigMetaQueryRequest {
4
+  page?: number;
5
+  pageSize?: number;
6
+  configName?: string;
7
+  fieldName?: string;
8
+  yamlName?: string;
9
+  sortField?: string;
10
+  sortOrder?: 'asc' | 'desc';
11
+}
12
+
13
+export interface PaginatedQueryResult<T> {
14
+  success: boolean;
15
+  data?: T;
16
+  totalCount?: number;
17
+}
18
+
19
+// 重新导出 ConfigMeta,以便向后兼容
20
+export type { ConfigMeta };

+ 17
- 17
src/app/pages/service-register-config/service-register-config.component.html Näytä tiedosto

@@ -16,29 +16,29 @@
16 16
     [debugRecordCount]="debugInfo.recordCount"
17 17
     [debugLastUpdated]="debugInfo.lastUpdated"
18 18
     [debugUseMockData]="debugInfo.useMockData"
19
-    [debugRegisterUrl]="lastRegisterUrl"
20
-    [debugListUrl]="lastListUrl"
19
+    [debugRegisterUrl]="debugInfo.registerUrl"
20
+    [debugListUrl]="debugInfo.listUrl"
21 21
     [headerWidth]="headerWidth"
22 22
     [headerLeft]="headerLeft"
23 23
     (buttonClick)="onRegister()"
24 24
   ></app-sticky-header>
25 25
    
26
-     <mat-card #matCard class="flex-1">
26
+      <mat-card #matCard class="flex-1">
27 27
     <mat-card-content>
28
-      <!-- Grid组件 -->
29
-      <app-grid
30
-        #gridRef
31
-        [dataUrl]="dataUrl"
32
-        [columns]="gridColumns"
33
-        [rowHeight]="48"
34
-        [showSummary]="false"
35
-        [showQueryPanel]="false"
36
-        [showPagination]="true"
37
-        [autoLoad]="true"
38
-        [loading]="loading"
39
-        (query)="onGridQuery($event)"
40
-        (dataLoaded)="onGridDataLoaded($event)">
41
-      </app-grid>
28
+        <!-- AG Grid组件 -->
29
+        <ag-grid-angular
30
+          class="ag-theme-alpine h-[calc(100vh-250px)]"
31
+          [columnDefs]="columnDefs"
32
+          [rowModelType]="'infinite'"
33
+          [datasource]="datasource"
34
+          [pagination]="true"
35
+          [paginationPageSize]="paginationPageSize"
36
+          [cacheBlockSize]="paginationPageSize"
37
+          [maxBlocksInCache]="10"
38
+          [domLayout]="'normal'"
39
+          [rowSelection]="'single'"
40
+          (gridReady)="onGridReady($event)">
41
+        </ag-grid-angular>
42 42
     </mat-card-content>
43 43
   </mat-card>
44 44
 </div>

+ 223
- 348
src/app/pages/service-register-config/service-register-config.component.ts Näytä tiedosto

@@ -1,401 +1,276 @@
1
-import { Component, OnInit, HostListener, ChangeDetectorRef, AfterViewInit, ElementRef, Renderer2, OnDestroy, ViewChild } from '@angular/core';
1
+import { Component, OnInit, ChangeDetectorRef, AfterViewInit, OnDestroy, ViewChild, ElementRef, Renderer2 } from '@angular/core';
2 2
 import { CommonModule } from '@angular/common';
3 3
 import { MatCard, MatCardContent } from '@angular/material/card';
4
-import { MatTableModule } from '@angular/material/table';
5 4
 
6 5
 import { HttpErrorResponse } from '@angular/common/http';
7
-import { StickyHeaderComponent, ConfigMetaService, ConfigService, ToastService, ConfigMeta, GridComponent, ColumnDefinition, formatTime } from 'ng-base';
6
+import { StickyHeaderComponent, ConfigMeta } from 'ng-base';
7
+import { ConfigMetaService } from '../../services/config-meta.service';
8
+import { AgGridAngular } from 'ag-grid-angular';
9
+import { ColDef, GridReadyEvent, PaginationChangedEvent, GridApi, IDatasource, IGetRowsParams, ModuleRegistry } from 'ag-grid-community';
10
+import { InfiniteRowModelModule, PaginationModule, RowSelectionModule } from 'ag-grid-community';
11
+import { ConfigMetaQueryRequest, PaginatedQueryResult } from '../../models/config-meta.model';
12
+
13
+// 注册 AG Grid 模块
14
+ModuleRegistry.registerModules([
15
+  InfiniteRowModelModule,
16
+  PaginationModule,
17
+  RowSelectionModule
18
+]);
8 19
 
9 20
 @Component({
10 21
   selector: 'app-service-register-config',
11
-  imports: [CommonModule, MatCard, MatCardContent, MatTableModule, StickyHeaderComponent, GridComponent],
22
+  imports: [CommonModule, MatCard, MatCardContent, StickyHeaderComponent, AgGridAngular],
12 23
   templateUrl: './service-register-config.component.html',
13 24
   styleUrl: './service-register-config.component.scss'
14 25
 })
15 26
 export class ServiceRegisterConfigComponent implements OnInit, AfterViewInit, OnDestroy {
16 27
   title = '注册服务配置';
17 28
   hintText = '点击"注册"按钮将同步所有配置元信息到数据库,并显示配置列表。';
18
-  displayedColumns: string[] = ['configName', 'fieldName', 'fieldType', 'yamlName', 'fieldDesc'];
19
-  configMetaList: ConfigMeta[] = [];
20
-  dataUrl: string = '';
21 29
   
22
-  // Grid配置
23
-  gridColumns: ColumnDefinition[] = [
24
-    { field: 'configName', title: '配置名称', dataType: 'string', width: 150, sortable: true, filterable: true },
25
-    { field: 'fieldName', title: '字段名称', dataType: 'string', width: 150, sortable: true, filterable: true },
26
-    { field: 'fieldType', title: '字段类型', dataType: 'string', width: 120, sortable: true, filterable: true },
27
-    { field: 'yamlName', title: 'YAML标签', dataType: 'string', width: 150, sortable: true, filterable: true },
28
-    { field: 'fieldDesc', title: '字段描述', dataType: 'string', width: 200, sortable: true, filterable: true }
30
+   // AG Grid 配置
31
+  columnDefs: ColDef[] = [
32
+    { field: 'id', headerName: 'ID', width: 120 },
33
+    { field: 'configName', headerName: '配置名称', width: 150 },
34
+    { field: 'fieldName', headerName: '字段名', width: 150 },
35
+    { field: 'fieldType', headerName: '字段类型', width: 120 },
36
+    { field: 'yamlName', headerName: 'YAML名称', width: 150 },
37
+    { field: 'fieldDesc', headerName: '字段描述', width: 200 },
38
+    { field: 'creator', headerName: '创建者', width: 120 },
39
+    { field: 'createdAt', headerName: '创建时间', width: 180 }
29 40
   ];
30
-  
41
+   rowData: any[] = [];
42
+   datasource: IDatasource | undefined = undefined;
43
+   configMetaList: ConfigMeta[] = [];
44
+
45
+   // 分页配置
46
+  paginationPageSize = 10;
47
+  currentPage = 1;
48
+  totalRecords = 0;
49
+  gridApi: GridApi | null = null;
50
+
31 51
   loading = false;
32 52
   registering = false;
33
-  isScrolled = false; // 向后兼容
34
-  isLocked = false;    // 是否锁定在顶部
35
-  isCompact = false;   // 是否缩小状态
36
-  scrollThreshold = 2; // 锁定阈值
37
-  compactThreshold = 50; // 缩小阈值(锁定后进一步滚动多少像素开始缩小)
38
-
39
-  private scrollContainer: HTMLElement | Window = window;
40
-  private scrollListener: (() => void) | null = null;
41
-  private rafId: number | null = null;
53
+  
54
+  // 顶部区域状态
55
+  isScrolled = false;
56
+  isLocked = false;
57
+  isCompact = false;
58
+  scrollThreshold = 2;
59
+  compactThreshold = 50;
60
+  headerWidth: number | null = null;
61
+  headerLeft: number | null = null;
42 62
 
43 63
   debugInfo = {
44 64
     dataSource: '',
45 65
     recordCount: 0,
46 66
     lastUpdated: '',
47
-    useMockData: false
67
+    useMockData: false,
68
+    registerUrl: '',
69
+    listUrl: '',
70
+    currentPage: 1,
71
+    totalPages: 1,
72
+    pageSize: 10
48 73
   };
49 74
 
75
+  @ViewChild(AgGridAngular) agGrid!: AgGridAngular;
50 76
   @ViewChild('matCard', { read: ElementRef }) matCardRef!: ElementRef;
51
-  @ViewChild('gridRef') gridRef!: GridComponent;
52
-  headerWidth: number | null = null;
53
-  headerLeft: number | null = null;
54
-  private resizeListener: (() => void) | null = null;
55
-
56
-  lastRegisterUrl = '';
57
-  lastListUrl = '';
58 77
 
59 78
   constructor(
60 79
     private configMetaService: ConfigMetaService,
61
-    private configService: ConfigService,
62
-    private toastService: ToastService,
63 80
     private cdRef: ChangeDetectorRef,
64 81
     private elementRef: ElementRef,
65 82
     private renderer: Renderer2
66 83
   ) {}
67 84
 
68
-  ngOnInit() {
69
-    console.log('ServiceRegisterConfigComponent initialized');
70
-    // 设置Grid数据API地址
71
-    this.dataUrl = this.configMetaService.getListUrl();
72
-    console.log('Grid dataUrl:', this.dataUrl);
73
-    
74
-    // 临时测试数据(仅作为备用显示)
75
-    if (this.configMetaList.length === 0) {
76
-      this.configMetaList = [
77
-        { id: '1', configName: 'db.yaml', fieldName: 'host', fieldType: 'string', yamlName: 'host', fieldDesc: '数据库主机', creator: 'system', createdAt: '2024-01-01' },
78
-        { id: '2', configName: 'db.yaml', fieldName: 'port', fieldType: 'number', yamlName: 'port', fieldDesc: '数据库端口', creator: 'system', createdAt: '2024-01-01' },
79
-        { id: '3', configName: 'db.yaml', fieldName: 'username', fieldType: 'string', yamlName: 'username', fieldDesc: '数据库用户名', creator: 'system', createdAt: '2024-01-01' }
80
-      ];
81
-    }
82
-    // 不再需要手动加载数据,Grid组件会自动加载
83
-    // this.loadConfigMeta();
84
-  }
85
-
86
-  ngAfterViewInit() {
87
-    this.findScrollContainer();
88
-    this.setupScrollListener();
89
-    this.checkScroll();
90
-    
91
-    // 添加窗口resize监听器
92
-    this.resizeListener = () => this.onResize();
93
-    window.addEventListener('resize', this.resizeListener, { passive: true });
94
-  }
95
-
96
-  private onResize() {
97
-    // 只在锁定状态时更新尺寸
98
-    if (this.isLocked && this.matCardRef?.nativeElement) {
99
-      console.log('窗口大小变化,更新标题栏尺寸');
100
-      this.updateHeaderDimensions();
101
-    }
102
-  }
103
-
104
-  ngOnDestroy() {
105
-    this.cleanupScrollListener();
106
-    
107
-    // 清理resize监听器
108
-    if (this.resizeListener) {
109
-      window.removeEventListener('resize', this.resizeListener);
110
-      this.resizeListener = null;
111
-    }
112
-  }
113
-
114
-  @HostListener('window:scroll', [])
115
-  onWindowScroll() {
116
-    // 保留window滚动监听作为备用
117
-    if (this.scrollContainer === window) {
118
-      this.onScroll();
119
-    }
120
-  }
121
-
122
-  private findScrollContainer() {
123
-    // 向上查找最近的滚动容器
124
-    let element: HTMLElement | null = this.elementRef.nativeElement.parentElement;
125
-    
126
-    while (element && element !== document.body) {
127
-      const style = getComputedStyle(element);
128
-      const hasOverflow = style.overflow === 'auto' || style.overflow === 'scroll' || 
129
-                          style.overflowY === 'auto' || style.overflowY === 'scroll';
130
-      
131
-      if (hasOverflow) {
132
-        console.log('找到滚动容器:', element, 'clientHeight:', element.clientHeight, 'scrollHeight:', element.scrollHeight);
133
-        this.scrollContainer = element;
134
-        return;
135
-      }
136
-      
137
-      element = element.parentElement;
138
-    }
139
-    
140
-    // 未找到合适的容器,使用window
141
-    console.log('未找到滚动容器,使用window');
142
-    this.scrollContainer = window;
143
-  }
144
-  
145
-  private setupScrollListener() {
146
-    if (this.scrollContainer === window) {
147
-      this.scrollListener = () => this.onScroll();
148
-      window.addEventListener('scroll', this.scrollListener, { passive: true });
149
-    } else {
150
-      this.scrollListener = () => this.onScroll();
151
-      (this.scrollContainer as HTMLElement).addEventListener('scroll', this.scrollListener, { passive: true });
152
-    }
153
-  }
154
-  
155
-  private cleanupScrollListener() {
156
-    if (this.scrollListener) {
157
-      if (this.scrollContainer === window) {
158
-        window.removeEventListener('scroll', this.scrollListener);
159
-      } else {
160
-        (this.scrollContainer as HTMLElement).removeEventListener('scroll', this.scrollListener);
161
-      }
162
-      this.scrollListener = null;
163
-    }
164
-    
165
-    if (this.rafId !== null) {
166
-      cancelAnimationFrame(this.rafId);
167
-      this.rafId = null;
168
-    }
169
-  }
170
-  
171
-  private onScroll() {
172
-    if (this.rafId !== null) {
173
-      cancelAnimationFrame(this.rafId);
174
-    }
175
-    
176
-    this.rafId = requestAnimationFrame(() => {
177
-      this.checkScroll();
178
-    });
179
-  }
180
-
181
-  private updateHeaderDimensions() {
182
-    if (!this.matCardRef?.nativeElement) return;
183
-    
184
-    const matCard = this.matCardRef.nativeElement;
185
-    const rect = matCard.getBoundingClientRect();
186
-    
187
-    // 计算mat-card在视口中的位置和宽度
188
-    this.headerWidth = rect.width;
189
-    this.headerLeft = rect.left;
190
-    
191
-    console.log('更新标题栏尺寸:', { 
192
-      width: this.headerWidth, 
193
-      left: this.headerLeft,
194
-      matCardRect: rect 
195
-    });
196
-    
197
-    this.cdRef.detectChanges();
198
-  }
199
-
200
-  private checkScroll() {
201
-    if (!this.scrollContainer) return;
202
-    
203
-    let scrollTop = 0;
204
-    let containerType = 'unknown';
205
-    
206
-    if (this.scrollContainer === window) {
207
-      scrollTop = window.scrollY || document.documentElement.scrollTop;
208
-      containerType = 'window';
209
-    } else {
210
-      scrollTop = (this.scrollContainer as HTMLElement).scrollTop;
211
-      containerType = 'element';
85
+    ngOnInit() {
86
+      console.log('ServiceRegisterConfigComponent initialized');
212 87
     }
213
-    
214
-    // 向后兼容:保持isScrolled逻辑
215
-    const scrolled = scrollTop > this.scrollThreshold;
216
-    
217 88
 
89
+  loadData(page: number = 1) {
90
+    this.loading = true;
91
+    this.currentPage = page;
218 92
     
219
-    // 阶段1: 检测是否锁定(开始滚动就锁定)
220
-    const shouldLock = scrollTop > 0;
221
-    
222
-    // 阶段2: 检测是否缩小(锁定后继续滚动)
223
-    const shouldCompact = shouldLock && scrollTop > this.compactThreshold;
224
-    
225
-    console.log('滚动检测:', { 
226
-      container: containerType, 
227
-      scrollTop, 
228
-      scrollThreshold: this.scrollThreshold,
229
-      compactThreshold: this.compactThreshold,
230
-      shouldLock,
231
-      shouldCompact,
232
-      scrolled, // 向后兼容
233
-      isLocked: this.isLocked,
234
-      isCompact: this.isCompact
235
-    });
236
-    
237
-    // 更新状态
238
-    let needsUpdate = false;
239
-    
240
-    if (this.isScrolled !== scrolled) {
241
-      this.isScrolled = scrolled;
242
-      needsUpdate = true;
243
-    }
244
-    
245
-    if (this.isLocked !== shouldLock) {
246
-      this.isLocked = shouldLock;
247
-      needsUpdate = true;
248
-      console.log('锁定状态变化:', this.isLocked);
249
-      
250
-      if (this.isLocked) {
251
-        // 锁定状态:计算并更新标题栏尺寸
252
-        this.updateHeaderDimensions();
253
-      } else {
254
-        // 解锁状态:清除尺寸,让标题栏恢复原始定位
255
-        this.headerWidth = null;
256
-        this.headerLeft = null;
257
-      }
258
-    }
93
+    // 构建查询请求
94
+    const request: ConfigMetaQueryRequest = {
95
+      page: page - 1, // API页码从0开始
96
+      pageSize: this.paginationPageSize
97
+    };
259 98
     
260
-    if (this.isCompact !== shouldCompact) {
261
-      this.isCompact = shouldCompact;
262
-      needsUpdate = true;
263
-      console.log('缩小状态变化:', this.isCompact);
264
-    }
99
+    console.log(`加载第${page}页数据,页大小: ${this.paginationPageSize}`);
265 100
     
266
-    if (needsUpdate) {
267
-      this.cdRef.detectChanges();
268
-    }
269
-  }
270
-
271
-  onRegister() {
272
-    this.lastRegisterUrl = this.configMetaService.getInitUrl();
273
-    console.debug('[注册] 请求URL:', this.lastRegisterUrl);
274
-    this.registering = true;
275
-    this.configMetaService.initConfigMeta().subscribe({
276
-      next: (response) => {
277
-        this.registering = false;
278
-        if (response.success) {
279
-          this.toastService.success('配置元信息注册成功');
280
-          // 刷新Grid数据
281
-          if (this.gridRef) {
282
-            this.gridRef.loadData();
283
-          } else {
284
-            console.warn('Grid组件引用未初始化');
101
+    this.configMetaService.listConfigMetaPaginated(request).subscribe({
102
+      next: (result: PaginatedQueryResult<ConfigMeta[]>) => {
103
+        this.loading = false;
104
+        
105
+        if (result.success && result.data) {
106
+          this.rowData = result.data;
107
+          this.configMetaList = result.data;
108
+          this.totalRecords = result.totalCount || result.data.length;
109
+          
110
+          // 更新AG Grid分页总记录数
111
+          if (this.gridApi) {
112
+            // 设置总记录数,使分页控件显示正确的总页数
113
+            this.gridApi.setGridOption('pagination', {
114
+              total: this.totalRecords,
115
+              pageSize: this.paginationPageSize
116
+            } as any);
285 117
           }
118
+          
119
+          // 更新调试信息
120
+          this.debugInfo.dataSource = 'API数据';
121
+          this.debugInfo.recordCount = this.totalRecords;
122
+          this.debugInfo.lastUpdated = new Date().toISOString();
123
+          this.debugInfo.useMockData = false;
124
+          this.debugInfo.registerUrl = this.configMetaService.getInitUrl();
125
+          this.debugInfo.listUrl = this.configMetaService.getListUrl();
126
+          this.debugInfo.currentPage = page;
127
+          this.debugInfo.totalPages = Math.ceil(this.totalRecords / this.paginationPageSize);
128
+          this.debugInfo.pageSize = this.paginationPageSize;
129
+          
130
+          console.log(`数据加载成功: ${result.data.length}条记录,总计: ${this.totalRecords}`);
286 131
         } else {
287
-          const errorMsg = this.extractErrorMessageFromResponse(response);
288
-          this.toastService.error(`注册失败:${errorMsg}`);
132
+          console.error('数据加载失败:', result);
289 133
         }
134
+        
135
+        this.cdRef.detectChanges();
290 136
       },
291
-      error: (error) => {
292
-        this.registering = false;
293
-        const errorMsg = this.extractErrorMessage(error);
294
-        this.toastService.error(`注册失败:${errorMsg}`);
295
-        console.debug('注册配置元信息失败', error);
137
+      error: (error: HttpErrorResponse) => {
138
+        this.loading = false;
139
+        console.error('加载数据时出错:', error);
140
+        this.cdRef.detectChanges();
296 141
       }
297 142
     });
298 143
   }
299 144
 
300
-  loadConfigMeta() {
301
-    this.lastListUrl = this.configMetaService.getListUrl();
302
-    console.debug('[加载] 请求URL:', this.lastListUrl);
303
-    
304
-    if (this.gridRef) {
305
-      // 使用Grid组件加载数据
306
-      this.gridRef.loadData();
307
-    } else {
308
-      // 向后兼容:手动加载数据
309
-      console.debug('Grid组件未初始化,使用手动加载');
310
-      this.loading = true;
311
-      this.configMetaService.listConfigMeta().subscribe({
312
-        next: (list) => {
313
-          this.configMetaList = list;
314
-          this.loading = false;
315
-          // 更新调试信息
316
-          const source = this.configService.useMockData ? '模拟数据' : 'API数据';
317
-          this.updateDebugInfo(list, source);
318
-        },
319
-        error: (error) => {
320
-          this.loading = false;
321
-          const errorMsg = this.extractErrorMessage(error);
322
-          this.toastService.error(`加载配置元信息失败:${errorMsg}`);
323
-          console.debug('加载配置元信息失败', error);
324
-          // 错误时也更新调试信息
325
-          this.updateDebugInfo([], `API调用失败: ${errorMsg}`);
326
-        }
327
-      });
328
-    }
329
-  }
330
-
331
-  // Grid事件处理
332
-  onGridQuery(queryParams: any) {
333
-    console.log('Grid查询参数:', queryParams);
334
-    // 这里可以处理查询事件,比如重新加载数据
335
-    // 由于当前使用的是静态数据,可以根据查询参数进行本地过滤
336
-  }
337
-
338
-  onGridDataLoaded(data: any[]) {
339
-    console.log('Grid数据加载完成:', data);
340
-    // 更新本地数据列表(向后兼容)
341
-    this.configMetaList = data as ConfigMeta[];
342
-    // 更新调试信息
343
-    const source = this.configService.useMockData ? '模拟数据' : 'API数据';
344
-    this.updateDebugInfo(this.configMetaList, source);
345
-  }
145
+   createInfiniteDatasource(): IDatasource {
146
+     return {
147
+       getRows: (params: IGetRowsParams) => {
148
+         const pageSize = this.paginationPageSize;
149
+         const startRow = params.startRow;
150
+         const page = Math.floor(startRow / pageSize);
151
+         
152
+         const request: ConfigMetaQueryRequest = {
153
+           page: page,
154
+           pageSize: pageSize
155
+         };
156
+         
157
+         console.log(`Infinite row model: startRow=${startRow}, endRow=${params.endRow}, page=${page}, pageSize=${pageSize}`);
158
+         
159
+         this.loading = true;
160
+         this.configMetaService.listConfigMetaPaginated(request).subscribe({
161
+           next: (result: PaginatedQueryResult<ConfigMeta[]>) => {
162
+             this.loading = false;
163
+             if (result.success && result.data) {
164
+               const rowData = result.data;
165
+               const totalCount = result.totalCount || 0;
166
+               
167
+               // 成功回调,提供行数据及总行数
168
+               // 对于无限行模型,需要传递 rowCount(总行数)以便网格知道何时停止加载
169
+               const lastRow = totalCount > 0 ? totalCount : -1;
170
+               params.successCallback(rowData, lastRow);
171
+               
172
+               // 更新调试信息
173
+               this.debugInfo.dataSource = 'API数据';
174
+               this.debugInfo.recordCount = totalCount;
175
+               this.debugInfo.lastUpdated = new Date().toISOString();
176
+               this.debugInfo.useMockData = false;
177
+               this.debugInfo.registerUrl = this.configMetaService.getInitUrl();
178
+               this.debugInfo.listUrl = this.configMetaService.getListUrl();
179
+               this.debugInfo.currentPage = page + 1;
180
+               this.debugInfo.totalPages = Math.ceil(totalCount / pageSize);
181
+               this.debugInfo.pageSize = pageSize;
182
+               
183
+               console.log(`无限行模型数据加载成功: ${rowData.length}条记录,总计: ${totalCount}`);
184
+               
185
+               // 更新组件状态
186
+               this.totalRecords = totalCount;
187
+               this.rowData = rowData;
188
+               this.configMetaList = rowData;
189
+             } else {
190
+               console.error('无限行模型数据加载失败:', result);
191
+               params.failCallback();
192
+             }
193
+             this.cdRef.detectChanges();
194
+           },
195
+           error: (error: HttpErrorResponse) => {
196
+             this.loading = false;
197
+             console.error('无限行模型加载数据时出错:', error);
198
+             params.failCallback();
199
+             this.cdRef.detectChanges();
200
+           }
201
+         });
202
+       },
203
+       rowCount: undefined // 初始时不知道总行数,将在第一次加载后由successCallback提供
204
+     };
205
+   }
346 206
 
207
+    onGridReady(params: GridReadyEvent) {
208
+      this.gridApi = params.api;
209
+      console.log('AG Grid ready, API initialized - setting infinite row model datasource');
210
+      
211
+      // 创建并设置无限行模型数据源
212
+      this.datasource = this.createInfiniteDatasource();
213
+      
214
+      // 通过API设置数据源(确保网格使用它)
215
+      params.api.setGridOption('datasource', this.datasource);
216
+      
217
+      // 添加分页事件监听器以更新调试信息
218
+      params.api.addEventListener('paginationChanged', this.onPaginationChanged.bind(this));
219
+    }
347 220
 
221
+   onPaginationChanged(event: PaginationChangedEvent) {
222
+     if (!this.gridApi || event.newPage === undefined || typeof event.newPage !== 'number') {
223
+       return;
224
+     }
225
+     
226
+     const newPage = event.newPage + 1; // AG Grid页码从0开始,转换为从1开始
227
+     if (newPage !== this.currentPage) {
228
+       console.log(`分页改变: 从第${this.currentPage}页到第${newPage}页`);
229
+       this.currentPage = newPage;
230
+       
231
+       // 更新调试信息中的当前页码
232
+       this.debugInfo.currentPage = newPage;
233
+     }
234
+   }
348 235
 
349
-  private updateDebugInfo(list: ConfigMeta[], source: string) {
350
-    // 轻量级调试信息更新,不影响性能
351
-    this.debugInfo = {
352
-      dataSource: source,
353
-      recordCount: list.length,
354
-      lastUpdated: formatTime(new Date()),
355
-      useMockData: this.configService.useMockData
356
-    };
357
-    
358
-    // 控制台调试日志(仅在开发时查看)
359
-    console.debug('[调试] 数据源:', source);
360
-    console.debug('[调试] 记录数:', list.length);
361
-    console.debug('[调试] 使用模拟数据:', this.configService.useMockData);
362
-    if (list.length > 0) {
363
-      console.debug('[调试] 首条数据示例:', list[0]);
364
-    }
236
+  ngAfterViewInit() {
237
+    console.log('ServiceRegisterConfigComponent view initialized');
365 238
   }
366 239
 
367
-  private extractErrorMessage(error: any): string {
368
-    if (error instanceof HttpErrorResponse) {
369
-      // HTTP错误
370
-      const status = error.status;
371
-      const errorBody = error.error;
372
-      console.debug(`[错误详情] HTTP ${status} ${error.url}`);
373
-      console.debug(`[错误详情] 错误响应:`, errorBody);
374
-      
375
-      if (errorBody?.error) {
376
-        return `HTTP ${status}: ${errorBody.error}`;
377
-      } else if (errorBody?.message) {
378
-        return `HTTP ${status}: ${errorBody.message}`;
379
-      } else if (error.message) {
380
-        return `HTTP ${status}: ${error.message}`;
381
-      } else {
382
-        return `HTTP错误 ${status}`;
383
-      }
384
-    } else if (error?.error) {
385
-      // 可能已经是QueryResult格式
386
-      return error.error.error || error.error.message || error.message || '未知错误';
387
-    } else if (error?.message) {
388
-      return error.message;
389
-    }
390
-    return '未知错误';
240
+  ngOnDestroy() {
241
+    console.log('ServiceRegisterConfigComponent destroyed');
391 242
   }
392 243
 
393
-  private extractErrorMessageFromResponse(response: any): string {
394
-    if (response?.error) {
395
-      return response.error;
396
-    } else if (response?.message) {
397
-      return response.message;
398
-    }
399
-    return '未知错误';
400
-  }
401
-}
244
+   onRegister() {
245
+     console.log('注册按钮被点击');
246
+     this.registering = true;
247
+     
248
+     this.configMetaService.initConfigMeta().subscribe({
249
+       next: (result: any) => {
250
+         this.registering = false;
251
+         if (result.success) {
252
+           console.log('注册完成:', result.data);
253
+           // 重新加载数据 - 刷新无限行模型缓存
254
+           if (this.gridApi) {
255
+             // 回到第一页
256
+             this.gridApi.paginationGoToFirstPage();
257
+             // 刷新缓存,强制重新加载数据
258
+             this.gridApi.refreshInfiniteCache();
259
+             
260
+             // 重置当前页码
261
+             this.currentPage = 1;
262
+             this.debugInfo.currentPage = 1;
263
+             
264
+             console.log('无限行模型缓存已刷新,回到第一页');
265
+           }
266
+         } else {
267
+           console.error('注册失败:', result);
268
+         }
269
+       },
270
+       error: (error: HttpErrorResponse) => {
271
+         this.registering = false;
272
+         console.error('注册时出错:', error);
273
+       }
274
+     });
275
+   }
276
+}

+ 16
- 0
src/app/services/agent.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { AgentService } from './agent.service';
4
+
5
+describe('AgentService', () => {
6
+  let service: AgentService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(AgentService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 9
- 0
src/app/services/agent.service.ts Näytä tiedosto

@@ -0,0 +1,9 @@
1
+import { Injectable } from '@angular/core';
2
+
3
+@Injectable({
4
+  providedIn: 'root'
5
+})
6
+export class AgentService {
7
+
8
+  constructor() { }
9
+}

+ 23
- 0
src/app/services/auth.service.spec.ts Näytä tiedosto

@@ -0,0 +1,23 @@
1
+import { TestBed } from '@angular/core/testing';
2
+import { provideHttpClient } from '@angular/common/http';
3
+import { provideHttpClientTesting } from '@angular/common/http/testing';
4
+import { AuthService } from './auth.service';
5
+
6
+describe('AuthService', () => {
7
+  let service: AuthService;
8
+
9
+  beforeEach(() => {
10
+    TestBed.configureTestingModule({
11
+      providers: [
12
+        provideHttpClient(),
13
+        provideHttpClientTesting(),
14
+        AuthService
15
+      ]
16
+    });
17
+    service = TestBed.inject(AuthService);
18
+  });
19
+
20
+  it('should be created', () => {
21
+    expect(service).toBeTruthy();
22
+  });
23
+});

+ 199
- 0
src/app/services/auth.service.ts Näytä tiedosto

@@ -0,0 +1,199 @@
1
+import { Injectable, inject } from '@angular/core';
2
+import { HttpClient } from '@angular/common/http';
3
+import { Observable, BehaviorSubject, throwError } from 'rxjs';
4
+import { tap, catchError } from 'rxjs/operators';
5
+import { Router } from '@angular/router';
6
+import { ConfigService } from './config.service';
7
+
8
+// 后端通用响应格式
9
+export interface QueryResult<T> {
10
+  success?: boolean;
11
+  data?: T;
12
+  message?: string;
13
+  error?: string;
14
+}
15
+
16
+export interface LoginResponse extends QueryResult<boolean> {
17
+  // /api/login/admin 返回 QueryResult[bool]
18
+  // token字段保留用于兼容性(可能某些API返回token)
19
+}
20
+
21
+export interface AuthState {
22
+  isAuthenticated: boolean;
23
+  user?: {
24
+    id: number;
25
+    username: string;
26
+    name?: string;
27
+    roles?: string[];
28
+  };
29
+  basicAuth?: string; // base64编码的 username:password
30
+}
31
+
32
+@Injectable({
33
+  providedIn: 'root'
34
+})
35
+export class AuthService {
36
+  private http = inject(HttpClient);
37
+  private router = inject(Router);
38
+  private configService = inject(ConfigService);
39
+
40
+  private authState = new BehaviorSubject<AuthState>({
41
+    isAuthenticated: false
42
+  });
43
+  authState$ = this.authState.asObservable();
44
+
45
+  private readonly BASIC_AUTH_KEY = 'basic_auth';
46
+  private readonly USER_KEY = 'auth_user';
47
+
48
+  constructor() {
49
+    console.log('AuthService初始化');
50
+    this.initializeAuthState();
51
+  }
52
+
53
+  private initializeAuthState() {
54
+    console.log('初始化认证状态');
55
+    const basicAuth = this.getBasicAuth();
56
+    const user = this.getUser();
57
+    console.log('本地存储Basic认证:', basicAuth ? '有' : '无');
58
+    console.log('本地存储用户:', user);
59
+
60
+    // 从本地存储恢复认证状态
61
+    if (basicAuth && user) {
62
+      console.log('从本地存储恢复认证状态');
63
+      this.authState.next({
64
+        isAuthenticated: true,
65
+        basicAuth,
66
+        user
67
+      });
68
+    } else {
69
+      console.log('未找到保存的认证信息,需要重新登录');
70
+      // 清除可能的不完整数据
71
+      if (basicAuth || user) {
72
+        console.log('清除不完整的认证数据');
73
+        this.clearAuthData();
74
+      }
75
+      // 初始状态设置为未认证
76
+      this.authState.next({
77
+        isAuthenticated: false
78
+      });
79
+    }
80
+  }
81
+
82
+  login(username: string, password: string): Observable<LoginResponse> {
83
+    console.log('开始登录,用户名:', username);
84
+    
85
+    // 调用管理员登录API
86
+    const apiUrl = `${this.configService.apiBaseUrl}/login/admin`;
87
+    console.log('登录API:', apiUrl);
88
+    
89
+    const loginData = {
90
+      user_id: username,
91
+      password: password
92
+    };
93
+    
94
+    return this.http.post<LoginResponse>(apiUrl, loginData).pipe(
95
+      tap(response => {
96
+        console.log('登录响应:', response);
97
+        
98
+        if (response.success) {
99
+          // 登录成功,生成Basic认证编码并保存
100
+          const basicAuth = btoa(`${username}:${password}`);
101
+          console.log('生成Basic认证编码:', basicAuth);
102
+          
103
+          this.setBasicAuth(basicAuth);
104
+          this.setUser({
105
+            id: 1,
106
+            username: username,
107
+            name: username,
108
+            roles: ['admin']
109
+          });
110
+          
111
+          this.authState.next({
112
+            isAuthenticated: true,
113
+            basicAuth,
114
+            user: {
115
+              id: 1,
116
+              username: username,
117
+              name: username,
118
+              roles: ['admin']
119
+            }
120
+          });
121
+          console.log('登录成功,认证状态更新');
122
+        } else {
123
+          console.warn('登录失败:', response.message || response.error);
124
+          throw new Error(response.message || response.error || '登录失败');
125
+        }
126
+      }),
127
+      catchError(error => {
128
+        console.error('登录失败:', error);
129
+        return throwError(() => error);
130
+      })
131
+    );
132
+  }
133
+
134
+  logout() {
135
+    console.log('用户登出');
136
+    this.clearAuthData();
137
+    this.authState.next({
138
+      isAuthenticated: false
139
+    });
140
+    this.router.navigate(['/login']);
141
+  }
142
+
143
+  isAuthenticated(): boolean {
144
+    return this.authState.value.isAuthenticated;
145
+  }
146
+
147
+  getBasicAuth(): string | null {
148
+    return localStorage.getItem(this.BASIC_AUTH_KEY);
149
+  }
150
+
151
+  private setBasicAuth(basicAuth: string) {
152
+    localStorage.setItem(this.BASIC_AUTH_KEY, basicAuth);
153
+  }
154
+
155
+  // 兼容性方法,返回null(不再使用token)
156
+  getToken(): string | null {
157
+    return null;
158
+  }
159
+
160
+  private setUser(user: any) {
161
+    if (user) {
162
+      localStorage.setItem(this.USER_KEY, JSON.stringify(user));
163
+    }
164
+  }
165
+
166
+  private getUser(): any {
167
+    const userStr = localStorage.getItem(this.USER_KEY);
168
+    if (userStr) {
169
+      try {
170
+        return JSON.parse(userStr);
171
+      } catch (e) {
172
+        console.error('解析用户信息失败:', e);
173
+        return null;
174
+      }
175
+    }
176
+    return null;
177
+  }
178
+
179
+  private clearAuthData() {
180
+    localStorage.removeItem(this.BASIC_AUTH_KEY);
181
+    localStorage.removeItem(this.USER_KEY);
182
+  }
183
+
184
+  // 获取当前用户信息
185
+  getCurrentUser() {
186
+    return this.authState.value.user;
187
+  }
188
+
189
+  // 检查是否具有特定角色
190
+  hasRole(role: string): boolean {
191
+    const user = this.authState.value.user;
192
+    if (!user || !user.roles) {
193
+      return false;
194
+    }
195
+    return user.roles.includes(role);
196
+  }
197
+
198
+  // 不再使用token刷新,使用Basic认证
199
+}

+ 187
- 0
src/app/services/config-meta.service.ts Näytä tiedosto

@@ -0,0 +1,187 @@
1
+import { Injectable } from '@angular/core';
2
+import { HttpClient } from '@angular/common/http';
3
+import { Observable, of } from 'rxjs';
4
+import { map } from 'rxjs/operators';
5
+import { ConfigMeta, ConfigMetaQueryRequest, PaginatedQueryResult } from '../models/config-meta.model';
6
+import { ConfigService } from './config.service';
7
+import { QueryResult } from './auth.service';
8
+
9
+@Injectable({
10
+  providedIn: 'root'
11
+})
12
+export class ConfigMetaService {
13
+  private initApiPath = '/init/config/meta';
14
+  private listApiPath = '/config/meta/list';
15
+
16
+  constructor(
17
+    private http: HttpClient,
18
+    private config: ConfigService
19
+  ) {
20
+    console.debug('[配置元服务] 初始化,apiBaseUrl:', this.config.apiBaseUrl);
21
+    console.debug('[配置元服务] useMockData:', this.config.useMockData);
22
+    console.debug('[配置元服务] initApiPath:', this.initApiPath);
23
+    console.debug('[配置元服务] listApiPath:', this.listApiPath);
24
+  }
25
+
26
+  getInitUrl(): string {
27
+    return `${this.config.apiBaseUrl}${this.initApiPath}`;
28
+  }
29
+
30
+  getListUrl(): string {
31
+    return `${this.config.apiBaseUrl}${this.listApiPath}`;
32
+  }
33
+
34
+  initConfigMeta(): Observable<QueryResult<any>> {
35
+    if (this.config.useMockData) {
36
+      console.debug('[配置元服务] 模拟初始化配置元信息');
37
+      return of({ success: true, data: '模拟初始化成功' });
38
+    }
39
+    const url = `${this.config.apiBaseUrl}${this.initApiPath}`;
40
+    console.debug(`[配置元服务] 调用初始化API: ${url}`);
41
+    console.debug(`[配置元服务] 完整URL: ${url}`);
42
+    return this.http.post<QueryResult<any>>(url, {});
43
+  }
44
+
45
+  listConfigMeta(): Observable<ConfigMeta[]> {
46
+    if (this.config.useMockData) {
47
+      console.debug('[配置元服务] 使用模拟数据');
48
+      const mockData = this.generateMockConfigMetaList();
49
+      console.debug(`[配置元服务] 模拟数据记录数: ${mockData.length}`);
50
+      return of(mockData);
51
+    }
52
+    
53
+    const url = `${this.config.apiBaseUrl}${this.listApiPath}`;
54
+    console.debug(`[配置元服务] 调用API: ${url}`);
55
+    console.debug(`[配置元服务] 完整URL: ${url}`);
56
+    return this.http.post<QueryResult<ConfigMeta[]>>(url, {})
57
+      .pipe(
58
+        map(response => {
59
+          const data = response.data || [];
60
+          console.debug(`[配置元服务] API响应记录数: ${data.length}`);
61
+          console.debug(`[配置元服务] API响应成功状态: ${response.success}`);
62
+          if (data.length > 0) {
63
+            console.debug('[配置元服务] 首条数据示例:', data[0]);
64
+          }
65
+          return data;
66
+        })
67
+      );
68
+  }
69
+
70
+  listConfigMetaPaginated(request: ConfigMetaQueryRequest): Observable<PaginatedQueryResult<ConfigMeta[]>> {
71
+    console.debug(`[配置元服务] listConfigMetaPaginated调用,useMockData: ${this.config.useMockData}`);
72
+    
73
+    if (this.config.useMockData) {
74
+      console.debug('[配置元服务] 使用模拟分页数据');
75
+      const mockResult = this.generateMockPaginatedData(request);
76
+      console.debug(`[配置元服务] 模拟数据: ${mockResult.data?.length || 0}条,总计: ${mockResult.totalCount || 0}`);
77
+      if (mockResult.data && mockResult.data.length > 0) {
78
+        console.debug('[配置元服务] 首条数据:', mockResult.data[0]);
79
+      }
80
+      return of(mockResult);
81
+    }
82
+    
83
+    const url = `${this.config.apiBaseUrl}${this.listApiPath}`;
84
+    console.debug(`[配置元服务] 调用真实API: ${url}`, request);
85
+    console.debug(`[配置元服务] 完整请求URL: ${url}`);
86
+    console.debug(`[配置元服务] 请求参数:`, JSON.stringify(request));
87
+    
88
+    return this.http.post<PaginatedQueryResult<ConfigMeta[]>>(url, request)
89
+      .pipe(
90
+        map(response => {
91
+          console.debug(`[配置元服务] API响应成功`);
92
+          console.debug(`[配置元服务] API响应记录数: ${response.data?.length || 0},总计: ${response.totalCount || 0}`);
93
+          console.debug(`[配置元服务] API响应成功状态: ${response.success}`);
94
+          if (response.data && response.data.length > 0) {
95
+            console.debug('[配置元服务] 首条API数据:', response.data[0]);
96
+          }
97
+          return response;
98
+        })
99
+      );
100
+  }
101
+
102
+  searchConfigMeta(configName?: string, fieldName?: string, yamlName?: string): Observable<ConfigMeta[]> {
103
+    if (this.config.useMockData) {
104
+      return of(this.generateMockConfigMetaList());
105
+    }
106
+    const params: any = {};
107
+    if (configName) params.configName = configName;
108
+    if (fieldName) params.fieldName = fieldName;
109
+    if (yamlName) params.yamlName = yamlName;
110
+    const url = `${this.config.apiBaseUrl}/config/meta/search`;
111
+    console.debug(`[配置元服务] 搜索API URL: ${url}`);
112
+    return this.http.post<QueryResult<ConfigMeta[]>>(url, params)
113
+      .pipe(
114
+        map(response => response.data || [])
115
+      );
116
+  }
117
+
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
+  }
138
+
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
+  }
187
+}

+ 16
- 0
src/app/services/config.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { ConfigService } from './config.service';
4
+
5
+describe('ConfigService', () => {
6
+  let service: ConfigService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(ConfigService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 25
- 0
src/app/services/config.service.ts Näytä tiedosto

@@ -0,0 +1,25 @@
1
+import { Injectable } from '@angular/core';
2
+
3
+@Injectable({
4
+  providedIn: 'root'
5
+})
6
+export class ConfigService {
7
+   private _useMockData = false; // 默认使用API数据
8
+  private _apiBaseUrl = '/api'; // 默认API基础URL,通过代理转发
9
+
10
+  get useMockData(): boolean {
11
+    return this._useMockData;
12
+  }
13
+
14
+  set useMockData(value: boolean) {
15
+    this._useMockData = value;
16
+  }
17
+
18
+  get apiBaseUrl(): string {
19
+    return this._apiBaseUrl;
20
+  }
21
+
22
+  set apiBaseUrl(value: string) {
23
+    this._apiBaseUrl = value;
24
+  }
25
+}

+ 16
- 0
src/app/services/markdown.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { MarkdownService } from './markdown.service';
4
+
5
+describe('MarkdownService', () => {
6
+  let service: MarkdownService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(MarkdownService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 110
- 0
src/app/services/markdown.service.ts Näytä tiedosto

@@ -0,0 +1,110 @@
1
+import { Injectable } from '@angular/core';
2
+import { HttpClient } from '@angular/common/http';
3
+import { Observable, catchError, map, of } from 'rxjs';
4
+import * as marked from 'marked';
5
+import * as Prism from 'prismjs';
6
+
7
+// 导入常用语言支持
8
+import 'prismjs/components/prism-javascript';
9
+import 'prismjs/components/prism-typescript';
10
+import 'prismjs/components/prism-go';
11
+import 'prismjs/components/prism-bash';
12
+import 'prismjs/components/prism-json';
13
+import 'prismjs/components/prism-yaml';
14
+import 'prismjs/components/prism-sql';
15
+import 'prismjs/components/prism-css';
16
+import 'prismjs/components/prism-scss';
17
+
18
+@Injectable({
19
+  providedIn: 'root'
20
+})
21
+export class MarkdownService {
22
+  private readonly basePath = 'docs/';
23
+
24
+  constructor(private http: HttpClient) {
25
+    // 配置marked选项和自定义渲染器
26
+    marked.setOptions({
27
+      gfm: true, // GitHub Flavored Markdown
28
+      breaks: true, // 将换行符转换为<br>
29
+      // sanitize选项在marked v17中已移除,使用外部HTML sanitizer替代
30
+      // 我们信任自己的内容,因此不进行额外清理
31
+    });
32
+  }
33
+
34
+  getMarkdown(fileName: string): Observable<string> {
35
+    const filePath = `${this.basePath}${fileName}`;
36
+    console.log('Requesting markdown file:', filePath);
37
+    
38
+    return this.http.get(filePath, { responseType: 'text' }).pipe(
39
+      map(markdown => this.parseMarkdown(markdown)),
40
+      catchError(error => {
41
+        console.error(`无法加载markdown文件: ${filePath}`, error);
42
+        console.error('Error details:', error.status, error.url, error.message);
43
+        return of(`<div class="text-red-500">无法加载文档: ${fileName}</div>`);
44
+      })
45
+    );
46
+  }
47
+
48
+  getConfigureReadme(): Observable<string> {
49
+    return this.getMarkdown('README.CONFIGURE.md');
50
+  }
51
+
52
+  private parseMarkdown(markdown: string): string {
53
+    try {
54
+      // 配置自定义渲染器支持代码高亮
55
+      const renderer = new marked.Renderer();
56
+      
57
+      // 重写code渲染器以支持prismjs语法高亮
58
+      // marked v17+ 使用对象参数 { text, lang, escaped }
59
+      const originalCode = renderer.code;
60
+      renderer.code = ({ text, lang, escaped }: { text: string; lang?: string; escaped?: boolean }): string => {
61
+        const validLanguage = this.getValidLanguage(lang);
62
+        const highlightedCode = validLanguage 
63
+          ? Prism.highlight(text, Prism.languages[validLanguage], validLanguage)
64
+          : this.escapeHtml(text);
65
+        
66
+        return `<pre class="prism-code language-${validLanguage || 'none'}"><code class="language-${validLanguage || 'none'}">${highlightedCode}</code></pre>`;
67
+      };
68
+      
69
+      // 使用自定义渲染器解析markdown
70
+      return marked.parse(markdown, { renderer }) as string;
71
+    } catch (error) {
72
+      console.error('Markdown解析错误:', error);
73
+      return `<pre class="p-4 bg-gray-100 rounded overflow-auto">${this.escapeHtml(markdown)}</pre>`;
74
+    }
75
+  }
76
+
77
+  private getValidLanguage(language: string | undefined): string | undefined {
78
+    if (!language) return undefined;
79
+    
80
+    // 清理语言标识,移除可能的前缀
81
+    const cleanLanguage = language.toLowerCase().trim();
82
+    
83
+    // 检查是否支持的语言
84
+    const supportedLanguages = ['javascript', 'typescript', 'js', 'ts', 'go', 'bash', 'sh', 'shell', 'json', 'yaml', 'yml', 'sql', 'css', 'scss', 'html', 'xml'];
85
+    
86
+    // 映射别名
87
+    const languageMap: Record<string, string> = {
88
+      'js': 'javascript',
89
+      'ts': 'typescript',
90
+      'sh': 'bash',
91
+      'shell': 'bash',
92
+      'yml': 'yaml',
93
+    };
94
+    
95
+    const mappedLanguage = languageMap[cleanLanguage] || cleanLanguage;
96
+    
97
+    // 检查是否在支持的语言列表中
98
+    if (supportedLanguages.includes(mappedLanguage) && Prism.languages[mappedLanguage]) {
99
+      return mappedLanguage;
100
+    }
101
+    
102
+    return undefined;
103
+  }
104
+
105
+  private escapeHtml(text: string): string {
106
+    const div = document.createElement('div');
107
+    div.textContent = text;
108
+    return div.innerHTML;
109
+  }
110
+}

+ 16
- 0
src/app/services/project.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { ProjectService } from './project.service';
4
+
5
+describe('ProjectService', () => {
6
+  let service: ProjectService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(ProjectService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 78
- 0
src/app/services/project.service.ts Näytä tiedosto

@@ -0,0 +1,78 @@
1
+import { Injectable } from '@angular/core';
2
+import { HttpClient } from '@angular/common/http';
3
+import { Observable, of } from 'rxjs';
4
+import { Project, ProjectRequest } from '../models/project.model';
5
+import { ConfigService } from './config.service';
6
+
7
+@Injectable({
8
+  providedIn: 'root'
9
+})
10
+export class ProjectService {
11
+  private apiPath = '/query/config/projects';
12
+
13
+  constructor(
14
+    private http: HttpClient,
15
+    private config: ConfigService
16
+  ) {}
17
+
18
+  getProjects(): Observable<Project[]> {
19
+    if (this.config.useMockData) {
20
+      return of(this.generateMockProjects());
21
+    }
22
+    return this.http.post<Project[]>(`${this.config.apiBaseUrl}${this.apiPath}`, {});
23
+  }
24
+
25
+  getProject(id: string): Observable<Project> {
26
+    if (this.config.useMockData) {
27
+      return of(this.generateMockProject(id));
28
+    }
29
+    return this.http.post<Project>(`${this.config.apiBaseUrl}/query/config/project/${id}`, {});
30
+  }
31
+
32
+  createProject(request: ProjectRequest): Observable<any> {
33
+    if (this.config.useMockData) {
34
+      return of({ success: true, data: 1 });
35
+    }
36
+    return this.http.post(`${this.config.apiBaseUrl}/create/config/project`, request);
37
+  }
38
+
39
+  updateProject(id: string, request: ProjectRequest): Observable<any> {
40
+    if (this.config.useMockData) {
41
+      return of({ success: true, data: 1 });
42
+    }
43
+    return this.http.post(`${this.config.apiBaseUrl}/update/config/project/${id}`, request);
44
+  }
45
+
46
+  deleteProject(id: string): Observable<any> {
47
+    if (this.config.useMockData) {
48
+      return of({ success: true, data: 1 });
49
+    }
50
+    return this.http.post(`${this.config.apiBaseUrl}/delete/config/project/${id}`, {});
51
+  }
52
+
53
+  private generateMockProjects(): Project[] {
54
+    const projects: Project[] = [];
55
+    for (let i = 1; i <= 10; i++) {
56
+      projects.push({
57
+        id: `project-${i}`,
58
+        name: `项目 ${i}`,
59
+        description: `这是第 ${i} 个项目的描述`,
60
+        tenant_id: `tenant-${(i % 3) + 1}`,
61
+        created_at: new Date(Date.now() - i * 86400000).toISOString(),
62
+        updated_at: new Date(Date.now() - i * 43200000).toISOString()
63
+      });
64
+    }
65
+    return projects;
66
+  }
67
+
68
+  private generateMockProject(id: string): Project {
69
+    return {
70
+      id,
71
+      name: `模拟项目 ${id}`,
72
+      description: `模拟项目描述`,
73
+      tenant_id: 'tenant-1',
74
+      created_at: new Date().toISOString(),
75
+      updated_at: new Date().toISOString()
76
+    };
77
+  }
78
+}

+ 16
- 0
src/app/services/role.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { RoleService } from './role.service';
4
+
5
+describe('RoleService', () => {
6
+  let service: RoleService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(RoleService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 78
- 0
src/app/services/role.service.ts Näytä tiedosto

@@ -0,0 +1,78 @@
1
+import { Injectable } from '@angular/core';
2
+import { HttpClient } from '@angular/common/http';
3
+import { Observable, of } from 'rxjs';
4
+import { Role, RoleRequest } from '../models/role.model';
5
+import { ConfigService } from './config.service';
6
+
7
+@Injectable({
8
+  providedIn: 'root'
9
+})
10
+export class RoleService {
11
+  private apiPath = '/query/config/roles';
12
+
13
+  constructor(
14
+    private http: HttpClient,
15
+    private config: ConfigService
16
+  ) {}
17
+
18
+  getRoles(): Observable<Role[]> {
19
+    if (this.config.useMockData) {
20
+      return of(this.generateMockRoles());
21
+    }
22
+    return this.http.post<Role[]>(`${this.config.apiBaseUrl}${this.apiPath}`, {});
23
+  }
24
+
25
+  getRole(id: string): Observable<Role> {
26
+    if (this.config.useMockData) {
27
+      return of(this.generateMockRole(id));
28
+    }
29
+    return this.http.post<Role>(`${this.config.apiBaseUrl}/query/config/role/${id}`, {});
30
+  }
31
+
32
+  createRole(request: RoleRequest): Observable<any> {
33
+    if (this.config.useMockData) {
34
+      return of({ success: true, data: 1 });
35
+    }
36
+    return this.http.post(`${this.config.apiBaseUrl}/create/config/role`, request);
37
+  }
38
+
39
+  updateRole(id: string, request: RoleRequest): Observable<any> {
40
+    if (this.config.useMockData) {
41
+      return of({ success: true, data: 1 });
42
+    }
43
+    return this.http.post(`${this.config.apiBaseUrl}/update/config/role/${id}`, request);
44
+  }
45
+
46
+  deleteRole(id: string): Observable<any> {
47
+    if (this.config.useMockData) {
48
+      return of({ success: true, data: 1 });
49
+    }
50
+    return this.http.post(`${this.config.apiBaseUrl}/delete/config/role/${id}`, {});
51
+  }
52
+
53
+  private generateMockRoles(): Role[] {
54
+    const roles: Role[] = [];
55
+    for (let i = 1; i <= 10; i++) {
56
+      roles.push({
57
+        id: `role-${i}`,
58
+        name: `角色 ${i}`,
59
+        description: `这是第 ${i} 个角色的描述`,
60
+        permissions: [`perm${i}-read`, `perm${i}-write`],
61
+        created_at: new Date(Date.now() - i * 86400000).toISOString(),
62
+        updated_at: new Date(Date.now() - i * 43200000).toISOString()
63
+      });
64
+    }
65
+    return roles;
66
+  }
67
+
68
+  private generateMockRole(id: string): Role {
69
+    return {
70
+      id,
71
+      name: `模拟角色 ${id}`,
72
+      description: `模拟角色描述`,
73
+      permissions: ['read', 'write'],
74
+      created_at: new Date().toISOString(),
75
+      updated_at: new Date().toISOString()
76
+    };
77
+  }
78
+}

+ 16
- 0
src/app/services/service-management.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { ServiceManagementService } from './service-management.service';
4
+
5
+describe('ServiceManagementService', () => {
6
+  let service: ServiceManagementService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(ServiceManagementService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 9
- 0
src/app/services/service-management.service.ts Näytä tiedosto

@@ -0,0 +1,9 @@
1
+import { Injectable } from '@angular/core';
2
+
3
+@Injectable({
4
+  providedIn: 'root'
5
+})
6
+export class ServiceManagementService {
7
+
8
+  constructor() { }
9
+}

+ 16
- 0
src/app/services/skill.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { SkillService } from './skill.service';
4
+
5
+describe('SkillService', () => {
6
+  let service: SkillService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(SkillService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 9
- 0
src/app/services/skill.service.ts Näytä tiedosto

@@ -0,0 +1,9 @@
1
+import { Injectable } from '@angular/core';
2
+
3
+@Injectable({
4
+  providedIn: 'root'
5
+})
6
+export class SkillService {
7
+
8
+  constructor() { }
9
+}

+ 16
- 0
src/app/services/tenant.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { TenantService } from './tenant.service';
4
+
5
+describe('TenantService', () => {
6
+  let service: TenantService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(TenantService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 76
- 0
src/app/services/tenant.service.ts Näytä tiedosto

@@ -0,0 +1,76 @@
1
+import { Injectable } from '@angular/core';
2
+import { HttpClient } from '@angular/common/http';
3
+import { Observable, of } from 'rxjs';
4
+import { Tenant, TenantRequest } from '../models/tenant.model';
5
+import { ConfigService } from './config.service';
6
+
7
+@Injectable({
8
+  providedIn: 'root'
9
+})
10
+export class TenantService {
11
+  private apiPath = '/query/config/tenants';
12
+
13
+  constructor(
14
+    private http: HttpClient,
15
+    private config: ConfigService
16
+  ) {}
17
+
18
+  getTenants(): Observable<Tenant[]> {
19
+    if (this.config.useMockData) {
20
+      return of(this.generateMockTenants());
21
+    }
22
+    return this.http.post<Tenant[]>(`${this.config.apiBaseUrl}${this.apiPath}`, {});
23
+  }
24
+
25
+  getTenant(id: string): Observable<Tenant> {
26
+    if (this.config.useMockData) {
27
+      return of(this.generateMockTenant(id));
28
+    }
29
+    return this.http.post<Tenant>(`${this.config.apiBaseUrl}/query/config/tenant/${id}`, {});
30
+  }
31
+
32
+  createTenant(request: TenantRequest): Observable<any> {
33
+    if (this.config.useMockData) {
34
+      return of({ success: true, data: 1 });
35
+    }
36
+    return this.http.post(`${this.config.apiBaseUrl}/create/config/tenant`, request);
37
+  }
38
+
39
+  updateTenant(id: string, request: TenantRequest): Observable<any> {
40
+    if (this.config.useMockData) {
41
+      return of({ success: true, data: 1 });
42
+    }
43
+    return this.http.post(`${this.config.apiBaseUrl}/update/config/tenant/${id}`, request);
44
+  }
45
+
46
+  deleteTenant(id: string): Observable<any> {
47
+    if (this.config.useMockData) {
48
+      return of({ success: true, data: 1 });
49
+    }
50
+    return this.http.post(`${this.config.apiBaseUrl}/delete/config/tenant/${id}`, {});
51
+  }
52
+
53
+  private generateMockTenants(): Tenant[] {
54
+    const tenants: Tenant[] = [];
55
+    for (let i = 1; i <= 10; i++) {
56
+      tenants.push({
57
+        id: `tenant-${i}`,
58
+        name: `租户 ${i}`,
59
+        description: `这是第 ${i} 个租户的描述`,
60
+        created_at: new Date(Date.now() - i * 86400000).toISOString(),
61
+        updated_at: new Date(Date.now() - i * 43200000).toISOString()
62
+      });
63
+    }
64
+    return tenants;
65
+  }
66
+
67
+  private generateMockTenant(id: string): Tenant {
68
+    return {
69
+      id,
70
+      name: `模拟租户 ${id}`,
71
+      description: `模拟租户描述`,
72
+      created_at: new Date().toISOString(),
73
+      updated_at: new Date().toISOString()
74
+    };
75
+  }
76
+}

+ 101
- 0
src/app/services/toast.service.ts Näytä tiedosto

@@ -0,0 +1,101 @@
1
+import { Injectable, ComponentRef, createComponent, ApplicationRef, Injector } from '@angular/core';
2
+import { ToastComponent } from '../components/toast/toast.component';
3
+
4
+export type ToastType = 'success' | 'error' | 'info' | 'warning';
5
+
6
+export interface ToastOptions {
7
+  duration?: number;
8
+  type?: ToastType;
9
+}
10
+
11
+@Injectable({
12
+  providedIn: 'root'
13
+})
14
+export class ToastService {
15
+  private toasts: ComponentRef<ToastComponent>[] = [];
16
+  private container: HTMLElement | null = null;
17
+
18
+  constructor(
19
+    private appRef: ApplicationRef,
20
+    private injector: Injector
21
+  ) {}
22
+
23
+  private ensureContainer() {
24
+    if (!this.container) {
25
+      this.container = document.createElement('div');
26
+      this.container.className = 'toast-container';
27
+      // 添加一些内联样式
28
+      this.container.style.position = 'fixed';
29
+      this.container.style.top = '20px';
30
+      this.container.style.right = '20px';
31
+      this.container.style.zIndex = '9999';
32
+      this.container.style.display = 'flex';
33
+      this.container.style.flexDirection = 'column';
34
+      this.container.style.alignItems = 'flex-end';
35
+      document.body.appendChild(this.container);
36
+    }
37
+  }
38
+
39
+  show(message: string, options?: ToastOptions) {
40
+    this.ensureContainer();
41
+    
42
+    const type = options?.type || 'info';
43
+    const duration = options?.duration || 3000;
44
+
45
+    const toastRef = createComponent(ToastComponent, {
46
+      environmentInjector: this.appRef.injector,
47
+      elementInjector: this.injector
48
+    });
49
+
50
+    // 设置输入属性(类型安全)
51
+    toastRef.instance.message = message;
52
+    toastRef.instance.type = type;
53
+
54
+    // 添加到DOM
55
+    this.container?.appendChild(toastRef.location.nativeElement);
56
+    this.appRef.attachView(toastRef.hostView);
57
+
58
+    // 存储引用
59
+    this.toasts.push(toastRef);
60
+
61
+    // 自动移除
62
+    setTimeout(() => {
63
+      this.removeToast(toastRef);
64
+    }, duration);
65
+
66
+    return toastRef;
67
+  }
68
+
69
+  success(message: string, options?: Omit<ToastOptions, 'type'>) {
70
+    return this.show(message, { ...options, type: 'success' });
71
+  }
72
+
73
+  error(message: string, options?: Omit<ToastOptions, 'type'>) {
74
+    return this.show(message, { ...options, type: 'error' });
75
+  }
76
+
77
+  info(message: string, options?: Omit<ToastOptions, 'type'>) {
78
+    return this.show(message, { ...options, type: 'info' });
79
+  }
80
+
81
+  warning(message: string, options?: Omit<ToastOptions, 'type'>) {
82
+    return this.show(message, { ...options, type: 'warning' });
83
+  }
84
+
85
+  private removeToast(toastRef: ComponentRef<ToastComponent>) {
86
+    const index = this.toasts.indexOf(toastRef);
87
+    if (index > -1) {
88
+      this.toasts.splice(index, 1);
89
+      this.appRef.detachView(toastRef.hostView);
90
+      toastRef.destroy();
91
+    }
92
+  }
93
+
94
+  clearAll() {
95
+    this.toasts.forEach(toastRef => {
96
+      this.appRef.detachView(toastRef.hostView);
97
+      toastRef.destroy();
98
+    });
99
+    this.toasts = [];
100
+  }
101
+}

+ 16
- 0
src/app/services/tree.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { TreeService } from './tree.service';
4
+
5
+describe('TreeService', () => {
6
+  let service: TreeService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(TreeService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 180
- 0
src/app/services/tree.service.ts Näytä tiedosto

@@ -0,0 +1,180 @@
1
+import { Injectable } from '@angular/core';
2
+import { HttpClient } from '@angular/common/http';
3
+import { Observable, of, catchError, map } from 'rxjs';
4
+import { TreeNode } from '../models/tree-node.model';
5
+import { ConfigService } from './config.service';
6
+
7
+@Injectable({
8
+  providedIn: 'root'
9
+})
10
+export class TreeService {
11
+  constructor(
12
+    private http: HttpClient,
13
+    private config: ConfigService
14
+  ) {}
15
+
16
+  getTree(): Observable<TreeNode[]> {
17
+    console.log('TreeService.getTree() 调用');
18
+    console.log('config.useMockData:', this.config.useMockData);
19
+    console.log('config.apiBaseUrl:', this.config.apiBaseUrl);
20
+    
21
+    if (this.config.useMockData) {
22
+      console.log('使用模拟树数据');
23
+      const mockTree = this.getMockTree();
24
+      console.log('模拟数据节点数:', mockTree.length);
25
+      // 调试:打印第一个节点的图标信息
26
+      if (mockTree.length > 0 && mockTree[0].children) {
27
+        console.log('[模拟数据]第一个分组节点图标:', mockTree[0].icon);
28
+        console.log('[模拟数据]第一个分组子节点图标:', mockTree[0].children.map(c => ({name: c.name, icon: c.icon})));
29
+      }
30
+      console.log('模拟数据结构:', JSON.stringify(mockTree, null, 2));
31
+      return of(mockTree);
32
+    }
33
+    
34
+    const apiUrl = `${this.config.apiBaseUrl}/projects/tree`;
35
+    console.log('请求树数据:', apiUrl);
36
+    
37
+    return this.http.get<any>(apiUrl).pipe(
38
+      map(response => {
39
+        console.log('树数据响应:', response);
40
+        // 后端返回 {success: true, data: [...]}
41
+        if (response && response.success && response.data) {
42
+          console.log('提取data字段,节点数:', response.data.length);
43
+          // 调试:打印第一个节点的图标信息
44
+          if (response.data.length > 0 && response.data[0].children) {
45
+            console.log('第一个分组节点图标:', response.data[0].icon);
46
+            console.log('第一个分组子节点图标:', response.data[0].children.map((c: any) => ({name: c.name, icon: c.icon})));
47
+          }
48
+          return response.data;
49
+        } else {
50
+          console.warn('响应格式不符合预期,返回空数组');
51
+          return [];
52
+        }
53
+      }),
54
+      catchError((error) => {
55
+        console.error('获取树数据失败:', error);
56
+        console.error('错误详情:', error.status, error.message, error.url);
57
+        // 认证失败或其他错误时返回空数组,不显示模拟数据
58
+        console.warn('API请求失败,回退到模拟数据');
59
+        const mockTree = this.getMockTree();
60
+        console.log('回退模拟数据节点数:', mockTree.length);
61
+        return of(mockTree);
62
+      })
63
+    );
64
+  }
65
+
66
+  private getMockTree(): TreeNode[] {
67
+    // 模拟树数据,基于Go后端返回的结构
68
+    return [
69
+      {
70
+        id: 'home',
71
+        name: '首页',
72
+        icon: 'home',
73
+        type: 'group',
74
+        children: [
75
+          {
76
+            id: 'readme',
77
+            name: '说明',
78
+            icon: 'description',
79
+            type: 'page',
80
+            route: '/home/readme'
81
+          }
82
+        ]
83
+      },
84
+      {
85
+        id: 'service-group',
86
+        name: '服务',
87
+        icon: 'settings',
88
+        type: 'group',
89
+        children: [
90
+          {
91
+            id: 'service-register',
92
+            name: '注册服务配置',
93
+            icon: 'app_registration',
94
+            type: 'page',
95
+            route: '/service/register-config'
96
+          },
97
+          {
98
+            id: 'service-management',
99
+            name: '微服务管理',
100
+            icon: 'dns',
101
+            type: 'page',
102
+            route: '/service/management'
103
+          },
104
+          {
105
+            id: 'service-config',
106
+            name: '微服务配置管理',
107
+            icon: 'settings_applications',
108
+            type: 'page',
109
+            route: '/service/config-management'
110
+          },
111
+          {
112
+            id: 'boot-config',
113
+            name: '微服务启动配置管理',
114
+            icon: 'play_circle',
115
+            type: 'page',
116
+            route: '/service/boot-config'
117
+          }
118
+        ]
119
+      },
120
+      {
121
+        id: 'user-group',
122
+        name: '项目',
123
+        icon: 'folder',
124
+        type: 'group',
125
+        children: [
126
+          {
127
+            id: 'project-management',
128
+            name: '项目管理',
129
+            icon: 'folder_open',
130
+            type: 'page',
131
+            route: '/project/list'
132
+          },
133
+          {
134
+            id: 'agent-management',
135
+            name: 'Agent管理',
136
+            icon: 'smart_toy',
137
+            type: 'page',
138
+            route: '/agent/list'
139
+          },
140
+          {
141
+            id: 'skill-management',
142
+            name: 'Skill管理',
143
+            icon: 'build',
144
+            type: 'page',
145
+            route: '/skill/list'
146
+          }
147
+        ]
148
+      },
149
+      {
150
+        id: 'tenant-group',
151
+        name: '租户管理',
152
+        icon: 'apartment',
153
+        type: 'group',
154
+        children: [
155
+          {
156
+            id: 'tenant-management',
157
+            name: '租户管理',
158
+            icon: 'apartment',
159
+            type: 'page',
160
+            route: '/tenant/list'
161
+          },
162
+          {
163
+            id: 'role-management',
164
+            name: '角色管理',
165
+            icon: 'admin_panel_settings',
166
+            type: 'page',
167
+            route: '/role/list'
168
+          },
169
+          {
170
+            id: 'user-management',
171
+            name: '用户管理',
172
+            icon: 'people',
173
+            type: 'page',
174
+            route: '/user/list'
175
+          }
176
+        ]
177
+      }
178
+    ];
179
+  }
180
+}

+ 16
- 0
src/app/services/user.service.spec.ts Näytä tiedosto

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { UserService } from './user.service';
4
+
5
+describe('UserService', () => {
6
+  let service: UserService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(UserService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 80
- 0
src/app/services/user.service.ts Näytä tiedosto

@@ -0,0 +1,80 @@
1
+import { Injectable } from '@angular/core';
2
+import { HttpClient } from '@angular/common/http';
3
+import { Observable, of } from 'rxjs';
4
+import { User, UserRequest } from '../models/user.model';
5
+import { ConfigService } from './config.service';
6
+
7
+@Injectable({
8
+  providedIn: 'root'
9
+})
10
+export class UserService {
11
+  private apiPath = '/query/config/users';
12
+
13
+  constructor(
14
+    private http: HttpClient,
15
+    private config: ConfigService
16
+  ) {}
17
+
18
+  getUsers(): Observable<User[]> {
19
+    if (this.config.useMockData) {
20
+      return of(this.generateMockUsers());
21
+    }
22
+    return this.http.post<User[]>(`${this.config.apiBaseUrl}${this.apiPath}`, {});
23
+  }
24
+
25
+  getUser(id: string): Observable<User> {
26
+    if (this.config.useMockData) {
27
+      return of(this.generateMockUser(id));
28
+    }
29
+    return this.http.post<User>(`${this.config.apiBaseUrl}/query/config/user/${id}`, {});
30
+  }
31
+
32
+  createUser(request: UserRequest): Observable<any> {
33
+    if (this.config.useMockData) {
34
+      return of({ success: true, data: 1 });
35
+    }
36
+    return this.http.post(`${this.config.apiBaseUrl}/create/config/user`, request);
37
+  }
38
+
39
+  updateUser(id: string, request: UserRequest): Observable<any> {
40
+    if (this.config.useMockData) {
41
+      return of({ success: true, data: 1 });
42
+    }
43
+    return this.http.post(`${this.config.apiBaseUrl}/update/config/user/${id}`, request);
44
+  }
45
+
46
+  deleteUser(id: string): Observable<any> {
47
+    if (this.config.useMockData) {
48
+      return of({ success: true, data: 1 });
49
+    }
50
+    return this.http.post(`${this.config.apiBaseUrl}/delete/config/user/${id}`, {});
51
+  }
52
+
53
+  private generateMockUsers(): User[] {
54
+    const users: User[] = [];
55
+    for (let i = 1; i <= 10; i++) {
56
+      users.push({
57
+        id: `user-${i}`,
58
+        username: `user${i}`,
59
+        email: `user${i}@example.com`,
60
+        tenant_id: `tenant-${(i % 3) + 1}`,
61
+        role_id: `role-${(i % 2) + 1}`,
62
+        created_at: new Date(Date.now() - i * 86400000).toISOString(),
63
+        updated_at: new Date(Date.now() - i * 43200000).toISOString()
64
+      });
65
+    }
66
+    return users;
67
+  }
68
+
69
+  private generateMockUser(id: string): User {
70
+    return {
71
+      id,
72
+      username: `模拟用户 ${id}`,
73
+      email: `mock${id}@example.com`,
74
+      tenant_id: 'tenant-1',
75
+      role_id: 'role-1',
76
+      created_at: new Date().toISOString(),
77
+      updated_at: new Date().toISOString()
78
+    };
79
+  }
80
+}

+ 4
- 0
src/styles.scss Näytä tiedosto

@@ -9,6 +9,10 @@
9 9
 /* Material Prebuilt Theme */
10 10
 @import '@angular/material/prebuilt-themes/indigo-pink.css';
11 11
 
12
+/* AG Grid Theme */
13
+@import 'ag-grid-community/styles/ag-grid.css';
14
+@import 'ag-grid-community/styles/ag-theme-alpine.css';
15
+
12 16
 /* You can add global styles to this file, and also import other style files */
13 17
 
14 18
 html, body { height: 100%; }

+ 2
- 2
tsconfig.json Näytä tiedosto

@@ -19,8 +19,8 @@
19 19
     "module": "ES2022",
20 20
     "baseUrl": "./",
21 21
     "paths": {
22
-      "ng-base": ["../ng-base/projects/ng-base/src/public-api.ts"],
23
-      "ng-base/*": ["../ng-base/projects/ng-base/src/lib/*"]
22
+      "ng-base": ["../ng-base/dist/ng-base"],
23
+      "ng-base/*": ["../ng-base/dist/ng-base/*"]
24 24
     }
25 25
   },
26 26
   "angularCompilerOptions": {

+ 12
- 0
vite.config.ts Näytä tiedosto

@@ -0,0 +1,12 @@
1
+import { defineConfig } from 'vite';
2
+import angular from '@analogjs/vite-plugin-angular';
3
+
4
+export default defineConfig({
5
+  plugins: [angular()],
6
+  optimizeDeps: {
7
+    include: ['ag-grid-angular', 'ag-grid-community']
8
+  },
9
+  ssr: {
10
+    noExternal: ['ag-grid-angular', 'ag-grid-community']
11
+  }
12
+});

Loading…
Peruuta
Tallenna