Browse Source

feat: optimize performance for risk items pick-up view

Signed-off-by: Carlos <568187512@qq.com>
Carlos 1 week ago
parent
commit
00f070d8e0

+ 19 - 11
src/app/pages/manager/hazard/hazard-tracking/hazard-tracking.component.ts

@@ -54,7 +54,7 @@ export class HazardTrackingComponent {
     data: undefined,
     isEdit: false,
   };
-  relativeTaskId?: number;
+  ids?: number[];
   isTaskRelative = false;
   constructor(
     private api: ApiService,
@@ -62,9 +62,9 @@ export class HazardTrackingComponent {
     private route: ActivatedRoute
   ) {
     this.route.queryParams.subscribe(params => {
-      if (params['taskId']) {
+      if (params['ids']) {
         this.isTaskRelative = true;
-        this.relativeTaskId = Number(params['taskId']);
+        this.ids = params['ids'].map(Number);
       }
     });
   }
@@ -73,14 +73,21 @@ export class HazardTrackingComponent {
   }
   getParams(): Hazard.GetHazardsParams {
     const { finalStatus, level, onlyMine } = this.search?.getParams() || {};
-    const params: Hazard.GetHazardsParams = {
-      page: this.pageConfig.page,
-      size: this.pageConfig.size,
-      company: this.setting.userCompany,
-      level,
-      finalStatus,
-      showMyList: onlyMine ? 1 : undefined,
-    };
+    const params: Hazard.GetHazardsParams = this.isTaskRelative
+      ? {
+          idList: this.ids,
+          page: this.pageConfig.page,
+          size: this.pageConfig.size,
+          company: this.setting.userCompany,
+        }
+      : {
+          page: this.pageConfig.page,
+          size: this.pageConfig.size,
+          company: this.setting.userCompany,
+          level,
+          finalStatus,
+          showMyList: onlyMine ? 1 : undefined,
+        };
     if (!level) {
       delete params.level;
     }
@@ -115,6 +122,7 @@ export class HazardTrackingComponent {
   }
   handleExitTaskRelative() {
     this.isTaskRelative = false;
+    this.ids = undefined;
     this.handleFetch();
   }
   onPageChange(page: number) {

+ 1 - 0
src/app/pages/manager/hazard/inspection-plan/detail/basic-info/basic-info.component.ts

@@ -241,6 +241,7 @@ export class BasicInfoComponent {
     switch (this.data.status) {
       case '未执行':
       case '制定中':
+      case '已制定':
         return this.authToModifyPlanBase;
       default:
         return false;

+ 1 - 1
src/app/pages/manager/hazard/inspection-plan/detail/detail-info/round-static-table/round-static-table.component.html

@@ -60,7 +60,7 @@
                     >排查完成,发现
                     <span
                       class="text-red-500 font-bold text-lg px-1 hover:underline hover:cursor-pointer"
-                      (click)="toHazardListPage(item.id)"
+                      (click)="toHazardListPage(item)"
                       >{{ item.checkOpinions?.length }}
                     </span>
                     处隐患</span

+ 4 - 2
src/app/pages/manager/hazard/inspection-plan/detail/detail-info/round-static-table/round-static-table.component.ts

@@ -89,8 +89,10 @@ export class RoundStaticTableComponent {
       nzOkText: '提交',
     });
   }
-  toHazardListPage(taskId: number) {
-    window.open(`/manager/potential-hazard/hazard-tracking?taskId=${taskId}`);
+  toHazardListPage(task: Hazard.InspectionPlanHandleDto) {
+    const handlers = task.checkOpinions || [];
+    const queries = handlers.map(item => `ids=${item.trouble.id}`).join('&');
+    window.open(`/manager/potential-hazard/hazard-tracking?${queries}`);
   }
   onSubmit(res: Hazard.ReportTaskParams) {
     return this.api.hazard.reportTask(res);

+ 7 - 1
src/app/pages/manager/hazard/inspection-plan/detail/detail.component.html

@@ -19,7 +19,13 @@
         <button class="precaution-danger !rounded-2xl" nz-button nzType="primary" (click)="handleWithdraw()">
           撤回
         </button>
-        <button class="precaution-secondary !rounded-2xl ml-4" nz-button nzType="primary" (click)="handleEdit()">
+        <button
+          class="precaution-secondary !rounded-2xl ml-4"
+          nz-button
+          nzType="primary"
+          [disabled]="disabledEdit"
+          (click)="handleEdit()"
+        >
           编辑
         </button>
       }

+ 10 - 0
src/app/pages/manager/hazard/inspection-plan/detail/detail.component.ts

@@ -61,4 +61,14 @@ export class InspectionPlanDetailComponent {
     const user = this.setting.user;
     return !!user && this.data.createUserId === user.id;
   }
+  get disabledEdit() {
+    switch (this.data.status) {
+      case '未执行':
+      case '制定中':
+      case '已制定':
+        return false;
+      default:
+        return true;
+    }
+  }
 }

+ 1 - 3
src/app/pages/manager/hazard/inspection-plan/plan-form/plan-form.component.html

@@ -109,9 +109,7 @@
         </button>
       </risk-item-picker>
       <div class="text-sm text-gray-500 px-4 pt-2">
-        @for (item of riskItemList; track item.id) {
-          <risk-item-visual class="mb-1" [data]="item"></risk-item-visual>
-        }
+        <risk-items-visual-classify [data]="riskItemList"></risk-items-visual-classify>
       </div>
     </nz-form-control>
   </nz-form-item>

+ 2 - 2
src/app/pages/manager/hazard/inspection-plan/plan-form/plan-form.component.ts

@@ -17,7 +17,7 @@ import {
 } from '../inspection-plan.utils';
 import { CommonNzModule } from './../../../../../common.nz.module';
 import { RiskItemPickerComponent } from './../../../../../shared/risk-item-picker/risk-item-picker.component';
-import { RiskItemVisualComponent } from './../../../../../shared/risk-item-picker/risk-item-visual/risk-item-visual.component';
+import { RiskItemsVisualClassifyComponent } from './../../../../../shared/risk-item-picker/risk-items-visual-classify/risk-items-visual-classify.component';
 
 export interface PlanFormComponentProps {
   data?: Hazard.InspectionPlan;
@@ -28,7 +28,7 @@ export interface PlanFormComponentProps {
 @Component({
   selector: 'plan-form',
   standalone: true,
-  imports: [CommonNzModule, RiskItemPickerComponent, RiskItemVisualComponent],
+  imports: [CommonNzModule, RiskItemPickerComponent, RiskItemsVisualClassifyComponent],
   templateUrl: './plan-form.component.html',
   styleUrl: './plan-form.component.less',
   host: {

+ 31 - 28
src/app/shared/risk-item-picker/modal/modal.component.html

@@ -41,41 +41,44 @@
         />
       </div>
     </div>
-    <div class="flex-1 text-sm limit-height pt-2 custom-scroll-bar">
-      @for (item of filteredItems; track item.id) {
-        <div class="px-3 pb-1">
-          <label
-            nz-checkbox
-            [nzValue]="item.id"
-            [nzChecked]="selectedItems.includes(item)"
-            (nzCheckedChange)="onItemChange(item.id, $event)"
-          >
-            <span>
-              <span>{{ item.title }}</span>
-            </span>
-          </label>
-        </div>
-      }
+    <div class="flex-1 text-sm limit-height relative custom-scroll-bar">
+      <div class="px-3 pb-1 shadow-lg sticky top-0 bg-white z-10 py-2">
+        <label
+          nz-checkbox
+          [(ngModel)]="allChecked"
+          [nzIndeterminate]="indeterminate"
+          (ngModelChange)="updateAllChecked()"
+          [nzDisabled]="isAllCheckboxDisabled"
+          class="mr-2"
+        >
+          全选 (当前共有 {{ filteredItems.length }} 条)
+        </label>
+      </div>
+      <div class="pt-2">
+        @for (item of filteredItems; track item.id) {
+          <div class="px-3 pb-1">
+            <label
+              nz-checkbox
+              [nzValue]="item.id"
+              [nzChecked]="selectedItems.includes(item)"
+              (nzCheckedChange)="onItemChange(item.id, $event)"
+            >
+              <span>
+                <span>{{ item.title }}</span>
+              </span>
+            </label>
+          </div>
+        }
+      </div>
     </div>
   </div>
   <div class="w-1/3 flex flex-col py-2">
     <div class="b-border pb-2 px-3 flex justify-between items-center">
-      <div class="text-base h-8 leading-8">已选择</div>
+      <div class="text-base h-8 leading-8">已选择 ({{ selectedItems.length }})</div>
       <button nz-button nzSize="small" class="precaution-secondary" nzType="primary" (click)="onClear()">清空</button>
     </div>
     <div class="relative flex-1 text-sm limit-height pt-2 custom-scroll-bar">
-      @for (item of selectedItems; track item.id) {
-        <div @horizontalInOut class="px-3 pb-2 flex justify-between">
-          <risk-item-visual [data]="item"></risk-item-visual>
-          <!-- <span class="pr-2">{{ item.title }}</span> -->
-          <span
-            nz-icon
-            nzType="close"
-            class="mx-1 text-gray-500 cursor-pointer transition-all hover:text-red-500 hover:rotate-180"
-            (click)="onRemove(item.id)"
-          ></span>
-        </div>
-      }
+      <risk-items-visual-classify [data]="selectedItems"></risk-items-visual-classify>
     </div>
   </div>
 </div>

+ 58 - 3
src/app/shared/risk-item-picker/modal/modal.component.ts

@@ -3,7 +3,7 @@ import { NZ_MODAL_DATA } from 'ng-zorro-antd/modal';
 import { horizontalInOut } from '../../../common.animation';
 import { KnowledgeService } from '../../../services/knowledge.service';
 import { CommonNzModule } from './../../../common.nz.module';
-import { RiskItemVisualComponent } from './../risk-item-visual/risk-item-visual.component';
+import { RiskItemsVisualClassifyComponent } from './../risk-items-visual-classify/risk-items-visual-classify.component';
 
 export interface RiskItemPickerModalData {
   title: string;
@@ -18,7 +18,7 @@ export interface RiskItemPickerModalData {
 @Component({
   selector: 'risk-item-picker-modal',
   standalone: true,
-  imports: [CommonNzModule, RiskItemVisualComponent],
+  imports: [CommonNzModule, RiskItemsVisualClassifyComponent],
   templateUrl: './modal.component.html',
   styleUrl: './modal.component.less',
   animations: [horizontalInOut],
@@ -63,23 +63,77 @@ export class RiskItemPickerModalComponent {
   onClear() {
     this.selectedItems = [];
     this.nzModalData.onChange([]);
+    this.allChecked = false;
+    this.indeterminate = false;
   }
   onItemChange(value: number, boolean = true) {
     const item = this.items.find(p => p.id === value);
     if (item) {
       if (boolean) {
-        this.selectedItems.unshift(item);
+        this.selectedItems = [item, ...this.selectedItems];
       } else {
         this.selectedItems = this.selectedItems.filter(p => p.id !== value);
       }
       this.nzModalData.onChange(this.selectedItems.map(p => p.id));
     }
+    this.updateAllCheckedStatus();
   }
 
   onRemove(itemId: number) {
     this.selectedItems = this.selectedItems.filter(p => p.id !== itemId);
+    this.updateAllCheckedStatus();
     this.nzModalData.onChange(this.selectedItems.map(p => p.id));
   }
+
+  allChecked = false;
+  indeterminate = false;
+
+  get isAllCheckboxDisabled(): boolean {
+    if (this.filteredItems.length === 0) {
+      return true;
+    }
+    if (!this.category && !this.type && !this.searchText) {
+      return true;
+    }
+    return false;
+  }
+
+  updateAllChecked(): void {
+    if (this.isAllCheckboxDisabled) return;
+
+    this.indeterminate = false;
+    if (this.allChecked) {
+      this.filteredItems.forEach(item => {
+        if (!this.selectedItems.includes(item)) {
+          this.selectedItems = [...this.selectedItems, item];
+        }
+      });
+    } else {
+      this.filteredItems.forEach(item => {
+        if (this.selectedItems.includes(item)) {
+          this.selectedItems = this.selectedItems.filter(p => p.id !== item.id);
+        }
+      });
+    }
+    this.nzModalData.onChange(this.selectedItems.map(p => p.id));
+  }
+
+  updateAllCheckedStatus(): void {
+    if (this.filteredItems.length === 0) {
+      this.allChecked = false;
+      this.indeterminate = false;
+    } else if (this.filteredItems.every(item => this.selectedItems.find(p => p.id === item.id))) {
+      this.allChecked = true;
+      this.indeterminate = false;
+    } else if (this.filteredItems.some(item => this.selectedItems.find(p => p.id === item.id))) {
+      this.allChecked = false;
+      this.indeterminate = true;
+    } else {
+      this.allChecked = false;
+      this.indeterminate = false;
+    }
+  }
+
   get title() {
     return this.nzModalData.title || '选择风险条目';
   }
@@ -98,6 +152,7 @@ export class RiskItemPickerModalComponent {
       result = result.filter(p => p.title.includes(this.searchText));
     }
     this.filteredItems = result;
+    this.updateAllCheckedStatus();
   }
   get types() {
     return this.nzModalData.types;

+ 39 - 0
src/app/shared/risk-item-picker/risk-items-visual-classify/risk-items-visual-classify.component.html

@@ -0,0 +1,39 @@
+@for (item of classifiedEasyArray; track $index) {
+  <div class="px-3 pb-2">
+    <div>
+      <span>
+        <span
+          nz-icon
+          nzType="right"
+          class="text-gray-500 cursor-pointer transition-all hover:text-blue-500 rotate-90 mr-2"
+          [ngClass]="{ 'rotate-0': item.collapsed }"
+          (click)="onToggleCollapse(item)"
+        ></span>
+      </span>
+      <span class="inline-block mb-1">
+        <span>
+          <nz-tag class="rounded mr-1" nzColor="blue"> {{ item.typeName }} - {{ item.categoryName }}</nz-tag>
+        </span>
+      </span>
+    </div>
+    @if (!item.collapsed) {
+      <div class="pl-4 pt-2">
+        @for (item of item.items; track $index) {
+          <div class="mb-2">
+            <span class="inline-block text-sm">
+              <span class="inline-block size-2 rounded-full bg-blue-500 mr-1"></span>
+              <span> {{ item.title }}</span>
+            </span>
+          </div>
+        }
+      </div>
+    }
+
+    <!-- <span
+      nz-icon
+      nzType="close"
+      class="mx-1 text-gray-500 cursor-pointer transition-all hover:text-red-500 hover:rotate-180"
+      (click)="onRemove(item.id)"
+      ></span> -->
+  </div>
+}

+ 0 - 0
src/app/shared/risk-item-picker/risk-items-visual-classify/risk-items-visual-classify.component.less


+ 23 - 0
src/app/shared/risk-item-picker/risk-items-visual-classify/risk-items-visual-classify.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RiskItemsVisualClassifyComponent } from './risk-items-visual-classify.component';
+
+describe('RiskItemsVisualClassifyComponent', () => {
+  let component: RiskItemsVisualClassifyComponent;
+  let fixture: ComponentFixture<RiskItemsVisualClassifyComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [RiskItemsVisualClassifyComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(RiskItemsVisualClassifyComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 94 - 0
src/app/shared/risk-item-picker/risk-items-visual-classify/risk-items-visual-classify.component.ts

@@ -0,0 +1,94 @@
+import { Component, Input, SimpleChanges } from '@angular/core';
+import { KnowledgeService } from '../../../services/knowledge.service';
+import { CommonNzModule } from './../../../common.nz.module';
+
+interface EasyClassified {
+  type: number;
+  typeName: string;
+  category: number;
+  categoryName: string;
+  items: Knowledge.RiskItem[];
+  collapsed: boolean;
+}
+
+@Component({
+  selector: 'risk-items-visual-classify',
+  standalone: true,
+  imports: [CommonNzModule],
+  templateUrl: './risk-items-visual-classify.component.html',
+  styleUrl: './risk-items-visual-classify.component.less',
+})
+export class RiskItemsVisualClassifyComponent {
+  @Input() data!: Knowledge.RiskItem[];
+
+  classified: Map<number, Map<number, Knowledge.RiskItem[]>> = new Map();
+  classifiedEasyArray: EasyClassified[] = [];
+
+  collapsedObject: { [key: string]: boolean } = {};
+
+  constructor(private knowledge: KnowledgeService) {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes['data']) {
+      this.setClassified();
+    }
+  }
+
+  setClassified() {
+    this.classified = new Map();
+    this.data.forEach(item => {
+      const classifiedByType = this.classified.get(item.type);
+      if (classifiedByType) {
+        const classifiedByCategory = classifiedByType.get(item.category);
+        if (classifiedByCategory) {
+          classifiedByType.set(item.category, [...classifiedByCategory, item]);
+        } else {
+          classifiedByType.set(item.category, [item]);
+        }
+      } else {
+        this.classified.set(item.type, new Map([[item.category, [item]]]));
+      }
+    });
+    this.classifiedEasyArray = [];
+    for (const [type, categoryMap] of this.classified.entries()) {
+      for (const [category, items] of categoryMap.entries()) {
+        this.classifiedEasyArray.push({
+          type: Number(type),
+          typeName: this.getTypeName(Number(type)) ?? '',
+          category: Number(category),
+          categoryName: this.getCategoryName(Number(category), Number(type)) ?? '',
+          items: items as Knowledge.RiskItem[],
+          collapsed: this.collapsedObject[`${type}-${category}`] ?? true,
+        });
+      }
+    }
+  }
+
+  onToggleCollapse(item: EasyClassified) {
+    item.collapsed = !item.collapsed;
+    this.collapsedObject[`${item.type}-${item.category}`] = item.collapsed;
+  }
+
+  getTypeName(type: number) {
+    return this.knowledge.getTypeName(type);
+  }
+
+  getCategoryName(category: number, type: number) {
+    return this.knowledge.getCategoryName(category, type);
+  }
+
+  get types() {
+    return this.knowledge.types();
+  }
+  get categories() {
+    return this.knowledge.categories();
+  }
+
+  get categoryIds() {
+    return this.data.map(p => p.category);
+  }
+
+  get typeIds() {
+    return this.data.map(p => p.type);
+  }
+}

+ 1 - 0
src/types/hazard.d.ts

@@ -122,6 +122,7 @@ declare namespace Hazard {
     showMyList?: number;
     page: number;
     size: number;
+    idList?: number[];
   }
 
   /**