Переглянути джерело

Merge branch 'dev' of http://www.lj2.top:3000/precaution/precaution-frontend into dev

mickey135 2 місяців тому
батько
коміт
c1a1a4233b
41 змінених файлів з 1295 додано та 64 видалено
  1. 1 1
      src/app/classes/risk-item.model.ts
  2. 2 24
      src/app/pages/manager/hazard/hazard-tracking/card/card.component.ts
  3. 218 0
      src/app/pages/manager/hazard/hazard-tracking/hazard-form/hazard-form.component.html
  4. 34 0
      src/app/pages/manager/hazard/hazard-tracking/hazard-form/hazard-form.component.less
  5. 23 0
      src/app/pages/manager/hazard/hazard-tracking/hazard-form/hazard-form.component.spec.ts
  6. 194 0
      src/app/pages/manager/hazard/hazard-tracking/hazard-form/hazard-form.component.ts
  7. 45 0
      src/app/pages/manager/hazard/hazard-tracking/hazard-search/hazard-search.component.html
  8. 18 0
      src/app/pages/manager/hazard/hazard-tracking/hazard-search/hazard-search.component.less
  9. 23 0
      src/app/pages/manager/hazard/hazard-tracking/hazard-search/hazard-search.component.spec.ts
  10. 39 0
      src/app/pages/manager/hazard/hazard-tracking/hazard-search/hazard-search.component.ts
  11. 7 3
      src/app/pages/manager/hazard/hazard-tracking/hazard-tracking.component.html
  12. 39 4
      src/app/pages/manager/hazard/hazard-tracking/hazard-tracking.component.ts
  13. 47 0
      src/app/pages/manager/hazard/hazard.utils.ts
  14. 84 1
      src/app/pages/manager/hazard/inspection-plan/inspection-plan.component.html
  15. 113 0
      src/app/pages/manager/hazard/inspection-plan/inspection-plan.component.less
  16. 124 2
      src/app/pages/manager/hazard/inspection-plan/inspection-plan.component.ts
  17. 19 0
      src/app/pages/manager/hazard/inspection-plan/inspection-plan.utils.ts
  18. 24 0
      src/app/pages/manager/hazard/inspection-plan/plan-card/plan-card.component.html
  19. 55 0
      src/app/pages/manager/hazard/inspection-plan/plan-card/plan-card.component.less
  20. 23 0
      src/app/pages/manager/hazard/inspection-plan/plan-card/plan-card.component.spec.ts
  21. 43 0
      src/app/pages/manager/hazard/inspection-plan/plan-card/plan-card.component.ts
  22. 6 2
      src/app/pages/manager/layout/header/header.component.html
  23. 10 0
      src/app/pages/manager/layout/header/header.component.ts
  24. 6 4
      src/app/pages/manager/layout/sidebar/navigation/navigation.component.ts
  25. 1 1
      src/app/pages/manager/risk/risk-bank/risk-item-form/risk-item-form.component.ts
  26. 1 1
      src/app/pages/manager/user/user-table/user-table.component.ts
  27. 1 1
      src/app/pages/manager/workbench/problem-assessment/detail/form/form.component.html
  28. 1 2
      src/app/pages/manager/workbench/problem-assessment/detail/form/form.component.ts
  29. 7 2
      src/app/services/basic.service.ts
  30. 13 0
      src/app/services/knowledge.service.ts
  31. 3 4
      src/app/services/setting.service.ts
  32. 7 1
      src/app/shared/custom-drawer/custom-drawer.component.html
  33. 7 0
      src/app/shared/custom-drawer/custom-drawer.component.less
  34. 3 3
      src/app/shared/issue-report-form/issue-report-form.component.html
  35. 1 1
      src/app/shared/issue-report-form/issue-report-form.component.ts
  36. 10 6
      src/app/shared/scroll-images/scroll-images.component.html
  37. 11 1
      src/app/shared/scroll-images/scroll-images.component.ts
  38. 1 0
      src/assets/icons/material-left-arrow.svg
  39. 1 0
      src/assets/icons/material-right-arrow.svg
  40. 19 0
      src/assets/icons/tool.svg
  41. 11 0
      src/types/hazard.d.ts

+ 1 - 1
src/app/classes/risk-item.model.ts

@@ -28,7 +28,7 @@ export class RiskItemModel {
     return this.basic.getEquipmentName(equipmentId);
   }
   getJobName(jobId: number) {
-    return this.basic.getJobName(jobId);
+    return this.basic.getJobNameWithDepartment(jobId);
   }
   getConsequenceName(consequenceId: number) {
     return this.basic.getConsequenceName(consequenceId);

+ 2 - 24
src/app/pages/manager/hazard/hazard-tracking/card/card.component.ts

@@ -2,30 +2,8 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
 import { CommonNzModule } from '../../../../../common.nz.module';
 import { BasicDataService } from '../../../../../services/basic.service';
 import { KnowledgeService } from '../../../../../services/knowledge.service';
+import { getHazardStatusText, hazardLevelColor, hazardStatusColor } from '../../hazard.utils';
 
-const hazardLevelColor: Record<string, string> = {
-  '重大': '#FA5F25',
-  'A': '#FFA100',
-  B: '#6600FF',
-  C: '#3083E6',
-};
-
-const hazardStatusColor: Record<string, string> = {
-  0: '#5E5E5E',
-  1: '#3083E6',
-  2: '#21A858',
-};
-const hazardStatusBgColor: Record<string, string> = {
-  0: '#5E5E5E66',
-  1: '#3083E666',
-  2: '#30E67A66',
-};
-
-const hazardStatusText: Record<string, string> = {
-  0: '已关闭',
-  1: '审核中',
-  2: '已处置',
-};
 @Component({
   selector: 'hazard-card',
   standalone: true,
@@ -58,7 +36,7 @@ export class HazardCardComponent {
       .join('、');
   }
   get hazardStatusText() {
-    return hazardStatusText[this.data.status];
+    return getHazardStatusText(this.data.status);
   }
   get hazardStatusBgColor() {
     return this.hazardStatusColor ? this.hazardStatusColor + '28' : '';

+ 218 - 0
src/app/pages/manager/hazard/hazard-tracking/hazard-form/hazard-form.component.html

@@ -0,0 +1,218 @@
+<form nz-form class="py-4" nzLayout="horizontal" [formGroup]="validateForm" (ngSubmit)="submitForm()">
+  <div nz-row>
+    <div nz-col [nzSpan]="10">
+      <nz-form-item>
+        <nz-form-label [nzSpan]="10" nzRequired>隐患级别 </nz-form-label>
+        <nz-form-control [nzSpan]="12" nzErrorTip="请选择隐患级别">
+          <nz-select class="precaution-select" nzPlaceHolder="请选择隐患级别" formControlName="level">
+            @for (level of levels; track level.value) {
+              <nz-option [nzValue]="level.value" [nzLabel]="level.label"></nz-option>
+            }
+          </nz-select>
+        </nz-form-control>
+      </nz-form-item>
+    </div>
+    <div nz-col [nzSpan]="10">
+      <nz-form-item>
+        <nz-form-label [nzSpan]="10" nzRequired>隐患类型 </nz-form-label>
+        <nz-form-control [nzSpan]="12" nzErrorTip="请选择隐患类型">
+          <nz-select class="precaution-select" nzPlaceHolder="请选择隐患类型" formControlName="type">
+            @for (level of types; track level.value) {
+              <nz-option [nzValue]="level.value" [nzLabel]="level.label"></nz-option>
+            }
+          </nz-select>
+        </nz-form-control>
+      </nz-form-item>
+    </div>
+  </div>
+  <div nz-row>
+    <div nz-col [nzSpan]="10">
+      <nz-form-item>
+        <nz-form-label nzRequired nzSpan="10">风险类别</nz-form-label>
+        <nz-form-control nzSpan="12" nzErrorTip="请选择风险类别">
+          <nz-select
+            class="precaution-select"
+            [nzOptions]="riskTypes"
+            formControlName="riskType"
+            nzPlaceHolder="请选择风险类别"
+            nzAllowClear
+            (ngModelChange)="changeRiskType($event)"
+          ></nz-select>
+        </nz-form-control>
+      </nz-form-item>
+    </div>
+
+    <div nz-col nzSpan="10">
+      <nz-form-item>
+        <nz-form-label nzRequired nzSpan="10">风险项</nz-form-label>
+        <nz-form-control nzSpan="12" nzErrorTip="请选择风险项">
+          <nz-select
+            class="precaution-select"
+            [nzOptions]="categories"
+            formControlName="riskCategory"
+            nzPlaceHolder="请选择风险项"
+            nzAllowClear
+            (ngModelChange)="changeRiskCategory($event)"
+          ></nz-select>
+        </nz-form-control>
+      </nz-form-item>
+    </div>
+  </div>
+  <nz-form-item>
+    <nz-form-label [nzSpan]="4" nzRequired>风险条目</nz-form-label>
+    <nz-form-control [nzSpan]="18" nzErrorTip="请选择风险条目">
+      <div class="pl-4">
+        <button
+          nz-button
+          class="precaution-button"
+          nzType="primary"
+          nzSize="small"
+          [disabled]="selectRiskItemDisabled"
+          (click)="selectRiskItem($event)"
+        >
+          {{ selectRiskItemDto ? '重新选择' : '选择' }}
+        </button>
+      </div>
+      @if (selectRiskItemDisabled) {
+        <div class="pl-4">
+          <span class="text-red-500">请先选择风险类别和风险项</span>
+        </div>
+      }
+      @if (selectRiskItemDto) {
+        <div class="py-1">
+          <div class="py-1 px-4 border border-solid border-primary rounded-md text-gray-700">
+            {{ selectRiskItemDto.title }}
+          </div>
+        </div>
+      }
+      <nz-select class="hidden" formControlName="riskItem"> </nz-select>
+    </nz-form-control>
+  </nz-form-item>
+  <div class="section-title">隐患详情信息</div>
+  <nz-form-item>
+    <nz-form-label [nzSpan]="4" nzRequired>隐患名称</nz-form-label>
+    <nz-form-control [nzSpan]="18" nzErrorTip="请输入隐患名称">
+      <input nz-input class="precaution-input" formControlName="name" placeHolder="请输入隐患名称" maxlength="100" />
+    </nz-form-control>
+  </nz-form-item>
+
+  <nz-form-item>
+    <nz-form-label [nzSpan]="4" nzRequired>问题描述</nz-form-label>
+    <nz-form-control [nzSpan]="18" nzErrorTip="请输入问题描述">
+      <textarea
+        nz-input
+        class="precaution-input"
+        formControlName="description"
+        rows="6"
+        placeHolder="请输入问题描述"
+        maxlength="1500"
+      ></textarea>
+    </nz-form-control>
+  </nz-form-item>
+  <nz-form-item>
+    <nz-form-label [nzSpan]="4" nzRequired>问题场所</nz-form-label>
+    <nz-form-control [nzSpan]="20" nzErrorTip="请选择问题场所">
+      <nz-space class="overflow-visible">
+        <nz-select
+          class="precaution-select"
+          style="min-width: 164px"
+          nzDropdownClassName="precaution-select-dropdown"
+          nzAllowClear
+          formControlName="position"
+          nzPlaceHolder="请选择问题场所"
+          (ngModelChange)="onInvolvedChange()"
+        >
+          @for (po of positions; track po.id) {
+            <nz-option [nzValue]="po.id" [nzLabel]="po.name"></nz-option>
+          }
+        </nz-select>
+
+        <nz-select
+          class="precaution-select ml-2"
+          style="min-width: 164px"
+          nzDropdownClassName="precaution-select-dropdown"
+          nzAllowClear
+          formControlName="operation"
+          nzPlaceHolder="请选择问题操作"
+          (ngModelChange)="onInvolvedChange()"
+        >
+          @for (op of operations; track op.id) {
+            <nz-option [nzValue]="op.id" [nzLabel]="op.name"></nz-option>
+          }
+        </nz-select>
+
+        <nz-select
+          class="precaution-select ml-2"
+          style="min-width: 164px"
+          nzDropdownClassName="precaution-select-dropdown"
+          nzAllowClear
+          formControlName="equipment"
+          nzPlaceHolder="请选择问题设备"
+          (ngModelChange)="onInvolvedChange()"
+        >
+          @for (eq of equipments; track eq.id) {
+            <nz-option [nzValue]="eq.id" [nzLabel]="eq.name"></nz-option>
+          }
+        </nz-select>
+      </nz-space>
+      <!-- </div> -->
+    </nz-form-control>
+  </nz-form-item>
+  <nz-form-item>
+    <nz-form-label [nzSpan]="4">具体地点</nz-form-label>
+    <nz-form-control [nzSpan]="18" nzErrorTip="请输入具体地点">
+      <input nz-input class="precaution-input" formControlName="spot" placeHolder="请输入具体地点" maxlength="50" />
+    </nz-form-control>
+  </nz-form-item>
+  <nz-form-item>
+    <nz-form-label [nzSpan]="4">现场照片</nz-form-label>
+    <nz-form-control [nzSpan]="18">
+      <image-upload-group #imageUploadGroup [fileList]="fileList"></image-upload-group>
+    </nz-form-control>
+  </nz-form-item>
+  <nz-form-item>
+    <nz-form-label [nzSpan]="4" nzRequired>流转部门</nz-form-label>
+    <nz-form-control [nzSpan]="6" nzErrorTip="请选择流转部门">
+      <nz-select
+        class="precaution-select"
+        nzDropdownClassName="precaution-select-dropdown"
+        formControlName="department"
+        nzPlaceHolder="请选择流转部门"
+      >
+        @for (dept of departments; track dept.id) {
+          <nz-option [nzValue]="dept.id" [nzLabel]="dept.name"></nz-option>
+        }
+      </nz-select>
+    </nz-form-control>
+  </nz-form-item>
+
+  <nz-form-item>
+    <nz-form-label [nzSpan]="4">流转人</nz-form-label>
+    <nz-form-control [nzSpan]="6">
+      <nz-select
+        class="precaution-select"
+        nzDropdownClassName="precaution-select-dropdown"
+        formControlName="responsible"
+        nzPlaceHolder="请选择流转人"
+      >
+        @for (user of users; track user.id) {
+          <nz-option [nzValue]="user.id" [nzLabel]="user.name"></nz-option>
+        }
+      </nz-select>
+    </nz-form-control>
+  </nz-form-item>
+  <div class="flex justify-end pr-8 py-4">
+    <button nz-button nzType="primary" class="precaution-secondary" (click)="onCancel()">取消</button>
+    <button nz-button class="precaution-button ml-4" nzType="primary">提交</button>
+  </div>
+</form>
+
+<ng-template #detailTpl>
+  <risk-item-detail
+    [data]="currentFilteredRiskItems"
+    [isCompare]="false"
+    [isSelect]="true"
+    [selected]="selectRiskItemId"
+    (selectChange)="handleSelectRiskItem($event)"
+  ></risk-item-detail>
+</ng-template>

+ 34 - 0
src/app/pages/manager/hazard/hazard-tracking/hazard-form/hazard-form.component.less

@@ -0,0 +1,34 @@
+:host {
+  display: block;
+  max-height: calc(100vh - 55px);
+  overflow-y: auto;
+}
+
+.section-title {
+  font-weight: 500;
+  font-size: 16px;
+  color: #131523;
+  line-height: 22px;
+  padding-left: 24px;
+  padding-bottom: 24px;
+}
+
+.noop-modal {
+  ::ng-deep &.ant-modal {
+    padding-bottom: 0;
+    .ant-modal-body {
+      padding: 0;
+    }
+    .ant-modal-content {
+      border-radius: 19px;
+    }
+    &.ant-modal .ant-modal-close {
+      // color: var(--ant-primary-color);
+      // top: 8px;
+      // right: 8px;
+      &:hover {
+        color: var(--ant-primary-color);
+      }
+    }
+  }
+}

+ 23 - 0
src/app/pages/manager/hazard/hazard-tracking/hazard-form/hazard-form.component.spec.ts

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

+ 194 - 0
src/app/pages/manager/hazard/hazard-tracking/hazard-form/hazard-form.component.ts

@@ -0,0 +1,194 @@
+import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
+import { AbstractControl, NonNullableFormBuilder, Validators } from '@angular/forms';
+import { NzMessageService } from 'ng-zorro-antd/message';
+import { NzModalService } from 'ng-zorro-antd/modal';
+import { NzUploadFile } from 'ng-zorro-antd/upload';
+import { CommonNzModule } from '../../../../../common.nz.module';
+import { ApiService } from '../../../../../services/api.service';
+import { BasicDataService } from '../../../../../services/basic.service';
+import { KnowledgeService } from '../../../../../services/knowledge.service';
+import { SettingService } from '../../../../../services/setting.service';
+import { ImageUploadGroupComponent } from '../../../../../shared/image-upload-group/image-upload-group.component';
+import { RiskItemDetailComponent } from '../../../risk/risk-bank/risk-item-detail/risk-item-detail.component';
+import { hazardLevelOptions, hazardTypeOptions } from '../../hazard.utils';
+
+export interface HazardFormComponentProps {
+  data?: Hazard.HazardDto;
+  visible: boolean;
+}
+
+@Component({
+  selector: 'hazard-form',
+  standalone: true,
+  imports: [CommonNzModule, ImageUploadGroupComponent, RiskItemDetailComponent],
+  templateUrl: './hazard-form.component.html',
+  styleUrl: './hazard-form.component.less',
+  host: {
+    class: 'custom-scroll-bar',
+  },
+})
+export class HazardFormComponent {
+  @ViewChild('detailTpl') detailTpl!: TemplateRef<void>;
+  @ViewChild('imageUploadGroup') imageUploadGroup!: ImageUploadGroupComponent;
+  @Input() data?: Hazard.HazardDto;
+  @Output() onClose = new EventEmitter<void>();
+  validateForm = this.fb.group({
+    name: this.fb.control<string>('', [Validators.required, Validators.maxLength(100)]),
+    level: this.fb.control<string>('', [Validators.required]),
+    type: this.fb.control<string>('', [Validators.required]),
+    riskType: this.fb.control<number | null>(null, [Validators.required]),
+    riskCategory: this.fb.control<number | null>(null, [Validators.required]),
+    riskItem: this.fb.control<number>(0, [Validators.required]),
+    description: this.fb.control<string>('', [Validators.required, Validators.maxLength(1500)]),
+    position: this.fb.control<number | null>(null, [Validators.required]),
+    spot: this.fb.control<string>('', []),
+    operation: this.fb.control<number | null>(null, []),
+    equipment: this.fb.control<number | null>(null, []),
+    department: this.fb.control<number | null>(null, []),
+    responsible: this.fb.control<number | null>(null, []),
+  });
+  levels: Option[] = hazardLevelOptions.slice();
+  types: Option[] = hazardTypeOptions.slice();
+  categories: Option[] = [];
+  departments: BasicData.Department[] = [];
+  uploading = false;
+  fileList: NzUploadFile[] = [
+    {
+      uid: '15',
+      name: '现场照片',
+      status: 'done',
+      url: 'https://precaution-check2.stage.leadinvr.com/api/uploadFile/get?name=tZuzRmxRvku74ccb898b08bdcbe77a050b20863d1cc6.jpg',
+      preview:
+        'https://precaution-check2.stage.leadinvr.com/api/uploadFile/get?name=tZuzRmxRvku74ccb898b08bdcbe77a050b20863d1cc6.jpg',
+    },
+  ];
+  constructor(
+    private viewContainerRef: ViewContainerRef,
+    private modal: NzModalService,
+    private fb: NonNullableFormBuilder,
+    private message: NzMessageService,
+    private setting: SettingService,
+    private api: ApiService,
+    private knowledge: KnowledgeService,
+    private basic: BasicDataService
+  ) {
+    setting.getUsersOfCurrentCompany();
+  }
+  involvedValidator(control: AbstractControl) {
+    if (!this.validateForm) {
+      return null;
+    }
+    const position = this.validateForm.get('position')?.value;
+    const operation = this.validateForm.get('operation')?.value;
+    const equipment = this.validateForm.get('equipment')?.value;
+    if (!position && !operation && !equipment) {
+      return { involved: true, error: true, required: true };
+    }
+    return null;
+  }
+  reset() {
+    this.validateForm.reset();
+    this.validateForm.markAsUntouched();
+    this.validateForm.updateValueAndValidity();
+
+    this.fileList = [];
+    this.uploading = false;
+  }
+  onCancel() {
+    this.onClose.emit();
+  }
+  onInvolvedChange() {
+    this.validateForm.get('position')?.updateValueAndValidity();
+    this.validateForm.get('operation')?.updateValueAndValidity();
+    this.validateForm.get('equipment')?.updateValueAndValidity();
+  }
+  selectRiskItem(ev: Event) {
+    ev.preventDefault();
+    this.modal.create<TemplateRef<void>, {}>({
+      nzContent: this.detailTpl,
+      nzViewContainerRef: this.viewContainerRef,
+      nzClassName: 'noop-modal',
+      nzClosable: true,
+      nzData: {},
+      nzWidth: 'calc(100vw - 144px)',
+      nzFooter: null,
+    });
+  }
+  async submitForm() {
+    if (!this.validateForm.valid) {
+      Object.values(this.validateForm.controls).forEach(control => {
+        if (control.invalid) {
+          control.markAsDirty();
+          control.updateValueAndValidity({ onlySelf: true });
+        }
+      });
+    } else {
+      this.onSubmit();
+    }
+  }
+  async onSubmit() {
+    console.log(this.validateForm.value);
+    console.log(this.imageUrls);
+    this.onClose.emit();
+  }
+  changeRiskType(t: number) {
+    // const { riskType } = this.validateForm.value;
+    this.validateForm.get('riskCategory')?.reset();
+    this.validateForm.get('department')?.reset();
+    this.validateForm.get('responsible')?.reset();
+    if (t) {
+      this.categories = this.knowledge.getCategoryOptionsByType(t);
+    } else {
+      this.categories = [];
+    }
+  }
+  changeRiskCategory(category: number) {
+    this.validateForm.get('department')?.reset();
+    this.validateForm.get('responsible')?.reset();
+    this.departments = this.knowledge.getDepartmentOptionsByCategory(category);
+  }
+  handleSelectRiskItem(id: number) {
+    this.validateForm.get('riskItem')?.setValue(id);
+  }
+  get positions() {
+    return this.basic.positions;
+  }
+  get operations() {
+    return this.basic.operations;
+  }
+  get equipments() {
+    return this.basic.equipments;
+  }
+  get users() {
+    const { department } = this.validateForm.value;
+    if (!department) return [];
+    return this.setting.usersOfCurrentCompany.filter(u => u.department === department);
+  }
+  get imageUrls() {
+    return this.imageUploadGroup.fileList.map(file => file.url);
+  }
+  get riskTypes() {
+    return this.knowledge.types().map(t => ({ label: t.value, value: t.id }));
+  }
+  get allRiskItems() {
+    return this.knowledge.riskItems();
+  }
+  get selectRiskItemId() {
+    return this.validateForm.get('riskItem')?.value;
+  }
+  get currentFilteredRiskItems() {
+    const { riskType, riskCategory } = this.validateForm.value;
+    if (!riskType || !riskCategory) return [];
+    return this.allRiskItems.filter(r => {
+      return r.category === riskCategory && r.type === riskType;
+    });
+  }
+  get selectRiskItemDto() {
+    return this.allRiskItems.find(r => r.id === this.selectRiskItemId);
+  }
+  get selectRiskItemDisabled() {
+    const { riskType, riskCategory } = this.validateForm.value;
+    if (!riskType || !riskCategory) return true;
+    return false;
+  }
+}

+ 45 - 0
src/app/pages/manager/hazard/hazard-tracking/hazard-search/hazard-search.component.html

@@ -0,0 +1,45 @@
+<div class="flex gap-4">
+  <span
+    class="link-select"
+    [class.active]="state.status !== -1"
+    nz-dropdown
+    [nzDropdownMenu]="statusMenu"
+    [nzOverlayStyle]="{ top: '-2px' }"
+    nzOverlayClassName="precaution-dropdown"
+  >
+    <span class="mr-2">{{ statusText }}</span>
+    <span nz-icon nzType="caret-down" nzTheme="outline" style="font-size: 10px"></span>
+  </span>
+
+  <span
+    class="link-select"
+    [class.active]="state.level !== ''"
+    nz-dropdown
+    [nzDropdownMenu]="levelMenu"
+    [nzOverlayStyle]="{ top: '2px' }"
+    nzOverlayClassName="precaution-dropdown"
+  >
+    <span class="mr-2">{{ levelText }}</span>
+    <span nz-icon nzType="caret-down" nzTheme="outline" style="font-size: 10px"></span>
+  </span>
+
+  <span>
+    <label nz-checkbox [(ngModel)]="state.onlyMine" (ngModelChange)="handleParamsChange()">只看我的</label>
+  </span>
+</div>
+
+<nz-dropdown-menu #statusMenu="nzDropdownMenu">
+  <ul nz-menu nzSelectable>
+    @for (item of statusOptions; track $index) {
+      <li nz-menu-item (click)="handleStatusChange(item.value)">{{ item.label }}</li>
+    }
+  </ul>
+</nz-dropdown-menu>
+
+<nz-dropdown-menu #levelMenu="nzDropdownMenu">
+  <ul nz-menu nzSelectable>
+    @for (item of levelOptions; track $index) {
+      <li nz-menu-item (click)="handleLevelChange(item.value)">{{ item.label }}</li>
+    }
+  </ul>
+</nz-dropdown-menu>

+ 18 - 0
src/app/pages/manager/hazard/hazard-tracking/hazard-search/hazard-search.component.less

@@ -0,0 +1,18 @@
+:host {
+  flex: 1;
+}
+
+.link-select {
+  display: inline-block;
+  min-width: 15%;
+  color: #333333;
+  font-size: 14px;
+  text-align: center;
+  cursor: pointer;
+  &:hover {
+    text-decoration: underline;
+  }
+  &.active {
+    color: var(--deep-blue);
+  }
+}

+ 23 - 0
src/app/pages/manager/hazard/hazard-tracking/hazard-search/hazard-search.component.spec.ts

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

+ 39 - 0
src/app/pages/manager/hazard/hazard-tracking/hazard-search/hazard-search.component.ts

@@ -0,0 +1,39 @@
+import { Component, EventEmitter, Output } from '@angular/core';
+import { CommonNzModule } from '../../../../../common.nz.module';
+import { hazardLevelOptions, hazardStatusOptions, SearchParams } from '../../hazard.utils';
+
+@Component({
+  selector: 'hazard-search',
+  standalone: true,
+  imports: [CommonNzModule],
+  templateUrl: './hazard-search.component.html',
+  styleUrl: './hazard-search.component.less',
+})
+export class HazardSearchComponent {
+  @Output() onChange = new EventEmitter<SearchParams>();
+  state: SearchParams = {
+    status: -1,
+    level: '',
+    onlyMine: false,
+  };
+  statusOptions = [{ value: -1, label: '全部状态' }, ...hazardStatusOptions];
+  levelOptions = [{ value: '', label: '隐患等级' }, ...hazardLevelOptions];
+
+  handleStatusChange(s: number) {
+    this.state.status = s;
+    this.onChange.emit(this.state);
+  }
+  handleLevelChange(l: string) {
+    this.state.level = l;
+    this.onChange.emit(this.state);
+  }
+  handleParamsChange() {
+    this.onChange.emit(this.state);
+  }
+  get statusText() {
+    return this.statusOptions.find(op => op.value === this.state.status)?.label || '全部状态';
+  }
+  get levelText() {
+    return this.levelOptions.find(op => op.value === this.state.level)?.label || '隐患等级';
+  }
+}

+ 7 - 3
src/app/pages/manager/hazard/hazard-tracking/hazard-tracking.component.html

@@ -1,9 +1,9 @@
 <div class="shared-panel">
   <div class="shared-panel-header">
     <div class="title w-1/5">隐患列表</div>
-    <div class="flex-1">123</div>
+    <hazard-search (onChange)="onSearchParamsChange($event)" />
     <div class="pr-4">
-      <button nz-button class="precaution-button add-button" nzType="primary">
+      <button nz-button class="precaution-button add-button" nzType="primary" (click)="handleAddHazard()">
         <span nz-icon nzType="plus" class="text-xs -mr-1"></span>
         <span class="text-base leading-normal">新增隐患</span>
       </button>
@@ -11,7 +11,7 @@
   </div>
   <div class="hazard-tracking-content custom-scroll-bar">
     <div class="grid grid-cols-4 gap-4">
-      @for (hazard of hazards; track hazard.id) {
+      @for (hazard of filteredHazards; track hazard.id) {
         <hazard-card [data]="hazard" (onViewDetail)="handleViewDetail($event)"></hazard-card>
       }
     </div>
@@ -25,3 +25,7 @@
     (onClose)="handleCloseDetail()"
   ></hazard-detail>
 }
+
+<custom-drawer [visible]="formConfig.visible" (onClose)="closeFormDrawer()" [width]="740" title="隐患基础信息">
+  <hazard-form #hazardForm [data]="formConfig.data" (onClose)="closeFormDrawer()"></hazard-form>
+</custom-drawer>

+ 39 - 4
src/app/pages/manager/hazard/hazard-tracking/hazard-tracking.component.ts

@@ -1,8 +1,12 @@
 import { Component } from '@angular/core';
 import { horizontalInOutReverse300ms } from '../../../../common.animation';
 import { CommonNzModule } from '../../../../common.nz.module';
+import { CustomDrawerComponent } from '../../../../shared/custom-drawer/custom-drawer.component';
+import { SearchParams } from '../hazard.utils';
 import { HazardCardComponent } from './card/card.component';
 import { HazardDetailComponent } from './detail/detail.component';
+import { HazardFormComponent, HazardFormComponentProps } from './hazard-form/hazard-form.component';
+import { HazardSearchComponent } from './hazard-search/hazard-search.component';
 
 export interface HazardDetailConfig {
   visible: boolean;
@@ -12,6 +16,8 @@ export interface HazardDetailConfig {
 const hazardLevels = ['重大', 'A', 'B', 'C'];
 const departments = Array.from({ length: 15 }, (_, i) => i + 1);
 
+const getRandomSize = (max: number) => Math.floor(Math.random() * max);
+
 const getMockHazard = (index: number): Hazard.HazardDto => {
   return {
     id: index,
@@ -24,27 +30,50 @@ const getMockHazard = (index: number): Hazard.HazardDto => {
     creator: '张三',
     reportingTime: '2024-09-25 12:00',
     responsible: 1,
-    responsibleDepartments: departments.slice(0, Math.floor(Math.random() * departments.length)),
-    images: Array.from({ length: 11 }).map(() => `https://loremflickr.com/260/150?q=${Math.random()}`),
+    responsibleDepartments: departments.slice(0, getRandomSize(departments.length)),
+    images: Array.from({ length: getRandomSize(11) }).map(() => `https://loremflickr.com/260/150?q=${Math.random()}`),
   };
 };
 
 @Component({
   selector: 'app-hazard-tracking',
   standalone: true,
-  imports: [CommonNzModule, HazardCardComponent, HazardDetailComponent],
+  imports: [
+    CommonNzModule,
+    HazardCardComponent,
+    HazardDetailComponent,
+    CustomDrawerComponent,
+    HazardFormComponent,
+    HazardSearchComponent,
+  ],
   templateUrl: './hazard-tracking.component.html',
   styleUrl: './hazard-tracking.component.less',
   animations: [horizontalInOutReverse300ms],
 })
 export class HazardTrackingComponent {
   hazards: Hazard.HazardDto[] = [];
+  filteredHazards: Hazard.HazardDto[] = [];
   detailConfig: HazardDetailConfig = {
     visible: false,
     data: {} as Hazard.HazardDto,
   };
+  formConfig: HazardFormComponentProps = {
+    visible: false,
+    data: undefined,
+  };
   ngOnInit() {
-    this.hazards = Array.from({ length: 2 }, (_, i) => getMockHazard(i));
+    this.hazards = Array.from({ length: getRandomSize(11) }, (_, i) => getMockHazard(i));
+    this.filteredHazards = this.hazards.slice();
+  }
+  onSearchParamsChange(p: SearchParams) {
+    const { status, level } = p;
+    this.filteredHazards = this.hazards.filter(h => {
+      if (status !== -1) {
+        if (h.status !== status) return false;
+      }
+      if (level && h.level !== level) return false;
+      return true;
+    });
   }
   handleViewDetail(data: Hazard.HazardDto) {
     this.detailConfig.visible = true;
@@ -53,4 +82,10 @@ export class HazardTrackingComponent {
   handleCloseDetail() {
     this.detailConfig.visible = false;
   }
+  handleAddHazard() {
+    this.formConfig.visible = true;
+  }
+  closeFormDrawer() {
+    this.formConfig.visible = false;
+  }
 }

+ 47 - 0
src/app/pages/manager/hazard/hazard.utils.ts

@@ -0,0 +1,47 @@
+export const hazardLevels = ['重大', 'A', 'B', 'C'];
+
+export const hazardLevelOptions: Option<string>[] = [
+  { label: '重大', value: '重大' },
+  { label: 'A类', value: 'A' },
+  { label: 'B类', value: 'B' },
+  { label: 'C类', value: 'C' },
+];
+
+export const hazardTypeOptions: Option[] = [
+  { label: '生产现场类隐患', value: 'product' },
+  { label: '基础管理类隐患', value: 'base' },
+];
+
+export const hazardLevelColor: Record<string, string> = {
+  '重大': '#FA5F25',
+  'A': '#FFA100',
+  B: '#6600FF',
+  C: '#3083E6',
+};
+
+export const hazardStatusColor: Record<string, string> = {
+  0: '#5E5E5E',
+  1: '#3083E6',
+  2: '#21A858',
+};
+export const hazardStatusBgColor: Record<string, string> = {
+  0: '#5E5E5E66',
+  1: '#3083E666',
+  2: '#30E67A66',
+};
+
+export const hazardStatusOptions: Option<number>[] = [
+  { value: 0, label: '已关闭' },
+  { value: 1, label: '审核中' },
+  { value: 2, label: '已处置' },
+];
+
+export const getHazardStatusText = (s: number) => {
+  return hazardStatusOptions.find(hz => hz.value === s)?.label || '';
+};
+
+export interface SearchParams {
+  status: number;
+  level: string;
+  onlyMine: boolean;
+}

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

@@ -1 +1,84 @@
-<p>inspection-plan works!</p>
+<div class="flex justify-between">
+  <div class="headline pl-2">排查计划</div>
+  <div class="pr-4">
+    <button nz-button class="precaution-secondary" nzType="primary" style="border-radius: 12px">创建计划</button>
+  </div>
+</div>
+<div class="flex">
+  <div class="overview-panel flex-1 flex h-[140px]">
+    <div class="w-[36%] r-border px-[3%]">
+      <div class="semi-annual-switch">
+        <div class="arrow" (click)="handleSemiAnnualChange('left')">
+          @if (leftArrowVisible) {
+            <span nz-icon nzType="icons:material-left-arrow" class="text-2xl cursor-pointer hover:text-primary"></span>
+          }
+        </div>
+        <div class="text">
+          {{ semiAnnualText }}
+        </div>
+        <div class="arrow" (click)="handleSemiAnnualChange('right')">
+          @if (rightArrowVisible) {
+            <span nz-icon nzType="icons:material-right-arrow" class="text-2xl cursor-pointer hover:text-primary"></span>
+          }
+        </div>
+      </div>
+      <div class="pb-2">
+        @for (row of firstSection; track $index) {
+          <div class="plan-type" [class.active]="currentPlanKey === row.key" (click)="handlePlanTypeChange(row.key)">
+            <span class="section-label">{{ row.label }}:</span>
+            <span class="unit-number">{{ row.value + '起' }}</span>
+          </div>
+        }
+      </div>
+    </div>
+    <div class="w-[64%] flex">
+      <div class="flex flex-col justify-between py-4 w-1/2 pl-[6%] pr-[4%]">
+        @for (row of secondSection; track $index) {
+          <div class="flex items-center h-1/3 justify-between">
+            <span class="section-label">{{ row.label }}:</span>
+            <span class="link-number text-left flex-1 pl-[20%]">{{ row.value + '%' }}</span>
+          </div>
+        }
+      </div>
+      <div class="flex flex-col py-6 w-1/2 pr-[8%] pl-[2%]">
+        <div class="flex justify-between">
+          <div class="section-label">排查中发现隐患:</div>
+          <div class="flex justify-between text-[#333333] font-medium w-[40%]">
+            <div>
+              <div class="pb-4">重大:{{ discovery.significant }}</div>
+              <div>A类:{{ discovery.a }}</div>
+            </div>
+            <div>
+              <div class="pb-4">B类:{{ discovery.b }}</div>
+              <div>C类:{{ discovery.c }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="overview-panel ml-5 w-[30%] pt-[13px]">
+    <div class="notifications">
+      @for (item of notifications; track $index) {
+        <div class="notification-item">
+          <div class="type">
+            <div
+              class="type-tag"
+              [style.color]="getNotificationTypeColor(item.type)"
+              [style.background-color]="getNotificationTypeBgColor(item.type)"
+            >
+              {{ getNotificationTypeText(item.type) }}
+            </div>
+          </div>
+          <div class="title">{{ item.title }}</div>
+          <div class="time pl-4">{{ item.createDate }}</div>
+        </div>
+      }
+    </div>
+  </div>
+</div>
+<div class="grid grid-cols-4 gap-5 py-5">
+  @for (plan of plans; track plan.id) {
+    <plan-card [data]="plan"></plan-card>
+  }
+</div>

+ 113 - 0
src/app/pages/manager/hazard/inspection-plan/inspection-plan.component.less

@@ -0,0 +1,113 @@
+.headline {
+  font-weight: 500;
+  font-size: 20px;
+  color: #000000;
+  line-height: 28px;
+}
+
+.overview-panel {
+  background: #ffffff;
+  box-shadow:
+    0px 0px 8px 0px rgba(223, 223, 223, 0.5),
+    0px 3px 4px 0px rgba(0, 0, 0, 0.04);
+  border-radius: 20px;
+}
+.r-border {
+  position: relative;
+  &:after {
+    content: '';
+    position: absolute;
+    display: block;
+    height: calc(100% - 32px);
+    right: 0;
+    top: 16px;
+    width: 1px;
+    background-color: rgba(151, 151, 151, 0.2);
+  }
+}
+
+.section-label {
+  font-weight: 400;
+  font-size: 14px;
+  color: #666666;
+}
+.link-number {
+  font-weight: 500;
+  font-size: 14px;
+  color: var(--deep-blue);
+  text-decoration: underline;
+}
+.unit-number {
+  font-weight: 500;
+  font-size: 14px;
+  color: #333333;
+  text-decoration: underline;
+}
+
+.plan-type {
+  display: flex;
+  justify-content: space-between;
+  border-radius: 8px;
+  background: transparent;
+  line-height: 32px;
+  padding: 0 8px;
+  transition: all 0.3s;
+  cursor: pointer;
+  &:hover {
+    background: rgba(41, 83, 232, 0.08);
+  }
+  &.active {
+    background: rgba(41, 83, 232, 0.16);
+  }
+}
+
+.semi-annual-switch {
+  display: flex;
+  padding: 10px 10% 0;
+  font-weight: 500;
+  font-size: 16px;
+  color: #333333;
+  line-height: 22px;
+  user-select: none;
+  .text {
+    flex: 1;
+    text-align: center;
+    user-select: none;
+  }
+  .arrow {
+    min-width: 24px;
+    user-select: none;
+  }
+}
+.notifications {
+  padding: 0 24px;
+}
+.notification-item {
+  text-wrap: nowrap;
+  display: flex;
+  flex-wrap: nowrap;
+  align-items: center;
+  margin-bottom: 8px;
+  &:last-child {
+    margin-bottom: 0;
+  }
+
+  .type-tag {
+    padding: 0 8px;
+    border-radius: 2px;
+    font-size: 14px;
+    line-height: 22px;
+  }
+
+  .title {
+    color: #4e5969;
+    font-size: 14px;
+    padding: 0 12px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+  .time {
+    font-size: 12px;
+    color: #999999;
+  }
+}

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

@@ -1,12 +1,134 @@
 import { Component } from '@angular/core';
+import { CommonNzModule } from '../../../../common.nz.module';
+import { PlanCardComponent } from './plan-card/plan-card.component';
+
+const notificationConfig = [
+  { type: 1, text: '通知', color: '#165DFF', backgroundColor: '#E8F3FF' },
+  { type: 2, text: '消息', color: '#0FC6C2', backgroundColor: '#E8FFFB' },
+];
+
+const departments = Array.from({ length: 15 }, (_, i) => i + 1);
+
+const getRandomSize = (max: number) => Math.floor(Math.random() * max);
+
+const getMockPlan = (index: number, name: string): Hazard.Plan => {
+  return {
+    id: index,
+    type: getRandomSize(4) || 1,
+    name: `${name}隐患排查计划`,
+    status: getRandomSize(4) || 1,
+    startDate: parseInt(name) + '/11/12',
+    endDate: parseInt(name) + '/11/14',
+    createDate: parseInt(name) + '/11/10',
+    department: getRandomSize(13) + 1,
+  };
+};
 
 @Component({
   selector: 'app-inspection-plan',
   standalone: true,
-  imports: [],
+  imports: [CommonNzModule, PlanCardComponent],
   templateUrl: './inspection-plan.component.html',
-  styleUrl: './inspection-plan.component.less'
+  styleUrl: './inspection-plan.component.less',
 })
 export class InspectionPlanComponent {
+  semiAnnualOptions = [
+    { label: '2023上半年度', value: 202301 },
+    { label: '2023下半年度', value: 202306 },
+    { label: '2024上半年度', value: 202401 },
+    { label: '2024下半年度', value: 202406 },
+    { label: '2025上半年度', value: 202501 },
+  ];
+  firstSection = [
+    { label: '日常隐患排查计划', value: 3, key: 1 },
+    { label: '专项隐患排查计划', value: 6, key: 2 },
+    { label: '其他隐患排查计划', value: 9, key: 3 },
+  ];
+  secondSection = [
+    { label: '计划站点排查覆盖', value: 65 },
+    { label: '其他场所排查覆盖', value: 100 },
+    { label: '计划人员排查占比', value: 95 },
+  ];
+  discovery = {
+    significant: 1,
+    a: 2,
+    b: 3,
+    c: 5,
+  };
+  notifications = [
+    {
+      type: 1,
+      title:
+        '当前产品试用期即将截止,在这段宝贵的试用时光里,您想必已经充分领略到了它的独特魅力与卓越性能。它犹如一位得力助手,在您的工作与生活中默默耕耘,助力您轻松跨越一道道难关,高效完成各项任务。无论是处理复杂的文档、分析海量的数据,还是畅享流畅的娱乐体验,它都表现得游刃有余',
+      createDate: '2024/6/16',
+    },
+    {
+      type: 1,
+      title:
+        '1月新系统升级计划通知,为了给您提供更优质、高效、稳定的服务体验,提升系统性能与功能,我们计划于 1 月进行系统升级。现将相关事宜详细通知如下:',
+      createDate: '2024/10/16',
+    },
+    {
+      type: 2,
+      title:
+        '新增内容已经通过审核,详情如下:此次审核涵盖了文本、图像、音频等多方面元素,确保了新增内容在质量与合规性上均达到了既定标准。文本部分,语言表达流畅自然,逻辑严谨清晰,无论是专业术语的运用还是普通语句的叙述,都经过了仔细的斟酌与校对,避免了歧义与错误信息的出现。',
+      createDate: '2024/11/4',
+    },
+    {
+      type: 1,
+      title:
+        '1月新系统升级计划通知为了给您提供更优质、高效、稳定的服务体验,提升系统性能与功能,我们计划于 1 月进行系统升级。现将相关事宜详细通知如下:',
+      createDate: '2024/11/16',
+    },
+  ];
+  currentPlanKey = 2;
+  semiAnnualKey = 202406;
+
+  plans: Hazard.Plan[] = Array.from({ length: getRandomSize(11) }, (_, i) =>
+    getMockPlan(i, this.getSemiAnnualLabel(this.semiAnnualKey))
+  );
+
+  getSemiAnnualLabel(key: number) {
+    return this.semiAnnualOptions.find(s => s.value === key)?.label || '2025上半年度';
+  }
+  handlePlanTypeChange(type: number) {
+    this.currentPlanKey = type;
+  }
+  handleSemiAnnualChange(direction: 'left' | 'right') {
+    const index = this.currentSemiAnnualOptionIndex;
+    if (direction === 'left') {
+      if (!this.leftArrowVisible) return;
+    }
+    if (direction === 'right') {
+      if (!this.rightArrowVisible) return;
+    }
+    this.semiAnnualKey = this.semiAnnualOptions[index + (direction === 'left' ? -1 : 1)]?.value || 0;
+  }
+
+  getNotificationTypeText(t: number) {
+    return notificationConfig.find(n => n.type === t)?.text || '未知';
+  }
+  getNotificationTypeColor(t: number) {
+    return notificationConfig.find(n => n.type === t)?.color || '';
+  }
+  getNotificationTypeBgColor(t: number) {
+    return notificationConfig.find(n => n.type === t)?.backgroundColor || '';
+  }
 
+  get currentSemiAnnualOptionIndex() {
+    return this.semiAnnualOptions.findIndex(se => se.value === this.semiAnnualKey);
+  }
+  get semiAnnualText() {
+    return this.semiAnnualOptions.find(se => se.value === this.semiAnnualKey)?.label || '';
+  }
+  get leftArrowVisible() {
+    const index = this.currentSemiAnnualOptionIndex;
+    if (index === -1) return false;
+    return !!this.semiAnnualOptions[index - 1];
+  }
+  get rightArrowVisible() {
+    const index = this.currentSemiAnnualOptionIndex;
+    if (index === -1) return false;
+    return !!this.semiAnnualOptions[index + 1];
+  }
 }

+ 19 - 0
src/app/pages/manager/hazard/inspection-plan/inspection-plan.utils.ts

@@ -0,0 +1,19 @@
+export const hazardPlanTypeOptions: Option<number>[] = [
+  { label: '日常隐患排查计划', value: 1 },
+  { label: '专项隐患排查计划', value: 2 },
+  { label: '其他隐患排查计划', value: 3 },
+];
+
+export const getHazardPlanTypeText = (v: number) => {
+  return hazardPlanTypeOptions.find(t => t.value === v)?.label || '';
+};
+
+export const hazardPlanStatusOptions: Option<number>[] = [
+  { label: '执行中', value: 1 },
+  { label: '已完成', value: 2 },
+  { label: '未开始', value: 3 },
+];
+
+export const getHazardPlanStatusText = (v: number) => {
+  return hazardPlanStatusOptions.find(t => t.value === v)?.label || '';
+};

+ 24 - 0
src/app/pages/manager/hazard/inspection-plan/plan-card/plan-card.component.html

@@ -0,0 +1,24 @@
+<div class="plan-card">
+  <div class="card-header">
+    <div class="flex-1">
+      <div class="title">
+        {{ data.name }}
+      </div>
+      <div class="type-row">
+        <span nz-icon nzType="icons:tool" class="text-primary align-bottom" style="font-size: 20px"></span>
+        <span> {{ typeText }}</span>
+      </div>
+    </div>
+    <div class="pt-4">
+      <div class="status">计划状态: {{ statusText }}</div>
+    </div>
+  </div>
+  <div class="px-[10px] py-2">
+    @for (item of infos; track $index) {
+      <div class="info-item">
+        <span class="label">{{ item.label }}:</span>
+        <span class="value">{{ item.value }}</span>
+      </div>
+    }
+  </div>
+</div>

+ 55 - 0
src/app/pages/manager/hazard/inspection-plan/plan-card/plan-card.component.less

@@ -0,0 +1,55 @@
+.plan-card {
+  box-shadow: 0px 2px 4px 2px rgba(53, 53, 53, 0.1);
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 12px 0 2px 8px;
+  transition: border 0.3s;
+  background-color: #ffffff;
+  position: relative;
+  &:hover {
+    border: 1px solid #2953e830;
+  }
+  .card-header {
+    @apply flex;
+  }
+  .title {
+    @apply flex-1  font-medium line-clamp-1;
+    font-weight: 500;
+    font-size: 18px;
+    color: #1d2129;
+  }
+  .type-row {
+    line-height: 20px;
+    padding-top: 16px;
+  }
+  .status {
+    display: inline-block;
+    background: #ffffff;
+    box-shadow:
+      0px 0px 8px 0px rgba(223, 223, 223, 0.5),
+      0px 2px 4px 0px rgba(187, 187, 187, 0.5);
+    border-radius: 12px 0px 0px 12px;
+    backdrop-filter: blur(1.838235294117647px);
+    font-weight: 400;
+    font-size: 10px;
+    color: #333333;
+    line-height: 14px;
+    text-shadow: 0px 0px 8px rgba(223, 223, 223, 0.5);
+    text-stroke: 0.5px #000000;
+    text-align: left;
+    font-style: normal;
+    -webkit-text-stroke: 0.5px #000000;
+    // padding: 8px 0 8px 16px;
+    padding-left: 16px;
+    padding-right: 4px;
+    line-height: 30px;
+  }
+}
+.info-item {
+  .label {
+    color: #333333;
+  }
+  .value {
+    color: #666666;
+  }
+}

+ 23 - 0
src/app/pages/manager/hazard/inspection-plan/plan-card/plan-card.component.spec.ts

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

+ 43 - 0
src/app/pages/manager/hazard/inspection-plan/plan-card/plan-card.component.ts

@@ -0,0 +1,43 @@
+import { Component, Input } from '@angular/core';
+import dayjs from 'dayjs';
+import { CommonNzModule } from '../../../../../common.nz.module';
+import { BasicDataService } from '../../../../../services/basic.service';
+import { getHazardPlanStatusText, getHazardPlanTypeText } from '../inspection-plan.utils';
+
+@Component({
+  selector: 'plan-card',
+  standalone: true,
+  imports: [CommonNzModule],
+  templateUrl: './plan-card.component.html',
+  styleUrl: './plan-card.component.less',
+})
+export class PlanCardComponent {
+  @Input() data!: Hazard.Plan;
+  constructor(private basic: BasicDataService) {
+    basic.init().then(() => {
+      this.setInfoValues();
+    });
+  }
+  ngOnInit() {
+    this.setInfoValues();
+  }
+  infos = [
+    { label: '排查周期', value: '' },
+    { label: '排查时间', value: '' },
+    { label: '发布时间', value: '' },
+    { label: '发布部门', value: '' },
+  ];
+  setInfoValues() {
+    const [du, time, createTime, d] = this.infos;
+    du.value = '周循环计划 每周1次';
+    time.value = `${dayjs(this.data.startDate).format('YYYY/MM/DD')}-${dayjs(this.data.endDate).format('YYYY/MM/DD')}`;
+    createTime.value = dayjs(this.data.createDate).format('YYYY/MM/DD');
+    d.value = this.basic.getDepartmentName(this.data.department);
+  }
+  get typeText() {
+    return getHazardPlanTypeText(this.data.type);
+  }
+  get statusText() {
+    return getHazardPlanStatusText(this.data.status);
+  }
+}

+ 6 - 2
src/app/pages/manager/layout/header/header.component.html

@@ -47,11 +47,15 @@
     </div>
     <div>
       <span class="text-[#8f8f8f]">责任部门:</span>
-      <span class="text-[#333333]">{{ user?.department }}</span>
+      <span class="text-[#333333]">{{ departmentText }}</span>
     </div>
     <div>
       <span class="text-[#8f8f8f]">责任场所:</span>
-      <span class="text-[#333333]">{{ user?.line }} - {{ user?.job }}</span>
+      <span class="text-[#333333]">{{ user?.line }}</span>
+    </div>
+    <div>
+      <span class="text-[#8f8f8f]">责任岗位:</span>
+      <span class="text-[#333333]"> {{ jobText }}</span>
     </div>
   </div>
 </div>

+ 10 - 0
src/app/pages/manager/layout/header/header.component.ts

@@ -86,4 +86,14 @@ export class HeaderComponent {
       .filter(Boolean)
       .join(',');
   }
+  get departmentText() {
+    const id = this.user?.department;
+    if (!id) return '';
+    return this.basic.getDepartmentName(id);
+  }
+  get jobText() {
+    const id = this.user?.job;
+    if (!id) return '';
+    return this.basic.getJobName(id);
+  }
 }

+ 6 - 4
src/app/pages/manager/layout/sidebar/navigation/navigation.component.ts

@@ -65,10 +65,12 @@ export class NavigationComponent {
       const secondaryRoute = this.findSecondaryRoute();
       if (secondaryRoute) {
         this.paths =
-          secondaryRoute.children?.map(child => ({
-            ...child,
-            path: `/manager/${secondaryRoute?.path}/${child.path}`,
-          })) || [];
+          secondaryRoute.children
+            ?.map(child => ({
+              ...child,
+              path: `/manager/${secondaryRoute?.path}/${child.path}`,
+            }))
+            .filter(p => !p.redirectTo) || [];
       }
     });
   }

+ 1 - 1
src/app/pages/manager/risk/risk-bank/risk-item-form/risk-item-form.component.ts

@@ -126,7 +126,7 @@ export class RiskItemFormComponent {
     this.options.job = this.basic.jobs
       .filter(j => this.currentCategory!.departments.includes(j.department))
       .map(item => ({
-        label: this.basic.getJobName(item.id),
+        label: this.basic.getJobNameWithDepartment(item.id),
         value: item.id,
       }));
   }

+ 1 - 1
src/app/pages/manager/user/user-table/user-table.component.ts

@@ -47,7 +47,7 @@ export class UserTableComponent {
     }
   }
   getJobName(jobId: number) {
-    return this.basic.jobs.find(j => j.id === jobId)?.name;
+    return this.basic.getJobName(jobId);
   }
   getDepartmentName(departmentId: number) {
     return this.basic.departments.find(d => d.id === departmentId)?.name;

+ 1 - 1
src/app/pages/manager/workbench/problem-assessment/detail/form/form.component.html

@@ -156,7 +156,7 @@
           formControlName="responsible"
           nzPlaceHolder="请选择流转人"
         >
-          @for (user of users | async; track user.id) {
+          @for (user of users; track user.id) {
             <nz-option [nzValue]="user.id" [nzLabel]="user.name"></nz-option>
           }
         </nz-select>

+ 1 - 2
src/app/pages/manager/workbench/problem-assessment/detail/form/form.component.ts

@@ -86,7 +86,6 @@ export class AssessmentFormComponent {
     });
   }
   handleSelectRiskItem(id: number) {
-    console.log(id);
     this.validateForm.get('riskItem')?.setValue(id);
   }
   async submitForm() {
@@ -125,7 +124,7 @@ export class AssessmentFormComponent {
     return this.basic.departments;
   }
   get users() {
-    return this.setting.usersOfCurrentCompany$;
+    return this.setting.usersOfCurrentCompany;
   }
   get isRisk() {
     return this.validateForm.get('result')?.value === 0;

+ 7 - 2
src/app/services/basic.service.ts

@@ -101,9 +101,9 @@ export class BasicDataService {
     return this.equipments.find(eq => eq.id === equipmentId)?.name;
   }
   getDepartmentName(departmentId: number) {
-    return this.departments.find(eq => eq.id === departmentId)?.name;
+    return this.departments.find(eq => eq.id === departmentId)?.name || '';
   }
-  getJobName(jobId: number) {
+  getJobNameWithDepartment(jobId: number) {
     const jobDto = this.jobs.find(job => job.id === jobId);
     if (!jobDto) return '';
     const departmentId = jobDto?.department;
@@ -111,6 +111,11 @@ export class BasicDataService {
     if (!departmentName) return jobDto.name;
     return `${jobDto.name}(${departmentName})`;
   }
+  getJobName(jobId: number) {
+    const jobDto = this.jobs.find(job => job.id === jobId);
+    if (!jobDto) return '';
+    return jobDto.name || '';
+  }
   getConsequenceName(consequenceId: number) {
     return this.consequences.find(con => con.id === consequenceId)?.value;
   }

+ 13 - 0
src/app/services/knowledge.service.ts

@@ -2,6 +2,7 @@ import { inject, Injectable, signal } from '@angular/core';
 import { Subject } from 'rxjs';
 import { getLevelColor } from '../app.util';
 import { ApiService } from './api.service';
+import { BasicDataService } from './basic.service';
 import { SettingService } from './setting.service';
 
 const mockLevels: Knowledge.RiskLevelDtoWithColor[] = [
@@ -158,6 +159,7 @@ const mockCategories: Knowledge.RiskTypeDto[] = [
 })
 export class KnowledgeService {
   private setting = inject(SettingService);
+  private basic = inject(BasicDataService);
 
   // levels
   private _levelUpdateDate = signal<string>(new Date().toISOString());
@@ -178,6 +180,8 @@ export class KnowledgeService {
   }
   onTypesChange = new Subject<void>();
 
+  categories = signal<Knowledge.RiskCategoryDto[]>([]);
+
   private _docs = signal<Knowledge.Doc[]>([]);
   docs = this._docs.asReadonly();
   get docsKeys() {
@@ -203,6 +207,12 @@ export class KnowledgeService {
     }));
   }
 
+  getDepartmentOptionsByCategory(category: number) {
+    const cateDto = this.categories().find(c => c.id === category);
+    if (!cateDto) return [];
+    return this.basic.departments.filter(d => cateDto.departments.includes(d.id));
+  }
+
   getTypeName(typeId: number) {
     return this.types().find(t => t.id === typeId)?.value;
   }
@@ -238,6 +248,9 @@ export class KnowledgeService {
     this.api.knowledge.getRiskTypes().then(res => {
       this._types.set(res.list);
       this._typeUpdateDate.set(res.updateDate);
+      this.categories.set([
+        ...res.list.reduce((acc, t) => [...acc, ...t.categories], [] as Knowledge.RiskCategoryDto[]),
+      ]);
       this.onTypesChange.next();
     });
   }

+ 3 - 4
src/app/services/setting.service.ts

@@ -1,5 +1,4 @@
 import { Injectable } from '@angular/core';
-import { from, map, Observable, of } from 'rxjs';
 import { ApiService } from './api.service';
 import { AuthService } from './auth.service';
 import { StorageService } from './storage.service';
@@ -10,7 +9,7 @@ import { StorageService } from './storage.service';
 export class SettingService {
   user?: Auth.User;
 
-  usersOfCurrentCompany$: Observable<Auth.User[]> = of([]);
+  usersOfCurrentCompany: Auth.User[] = [];
 
   get userCompany() {
     return this.user?.company || '';
@@ -28,8 +27,8 @@ export class SettingService {
     this._initialized = false;
     this.user = undefined;
   }
-  getUsersOfCurrentCompany() {
-    this.usersOfCurrentCompany$ = from(this.api.user.getAllUserByCompany(this.userCompany)).pipe(map(res => res));
+  async getUsersOfCurrentCompany() {
+    this.usersOfCurrentCompany = await this.api.user.getAllUserByCompany(this.userCompany);
   }
   async init(): Promise<boolean> {
     if (this._initialized) {

+ 7 - 1
src/app/shared/custom-drawer/custom-drawer.component.html

@@ -5,7 +5,7 @@
   [nzWidth]="width"
   [nzVisible]="visible"
   nzPlacement="right"
-  [nzTitle]="title === null ? undefined : title"
+  [nzTitle]="title === null ? undefined : titleTpl"
   (nzOnClose)="close()"
 >
   <ng-container *nzDrawerContent>
@@ -15,3 +15,9 @@
     }
   </ng-container>
 </nz-drawer>
+
+<ng-template #titleTpl>
+  <div class="drawer-title">
+    {{ title }}
+  </div>
+</ng-template>

+ 7 - 0
src/app/shared/custom-drawer/custom-drawer.component.less

@@ -44,3 +44,10 @@
     }
   }
 }
+
+.drawer-title {
+  font-weight: 500;
+  font-size: 16px;
+  color: #131523;
+  line-height: 22px;
+}

+ 3 - 3
src/app/shared/issue-report-form/issue-report-form.component.html

@@ -120,7 +120,7 @@
         formControlName="responsible"
         nzPlaceHolder="请选择流转人"
       >
-        @for (user of users | async; track user.id) {
+        @for (user of users; track user.id) {
           <nz-option [nzValue]="user.id" [nzLabel]="user.name"></nz-option>
         }
       </nz-select>
@@ -128,7 +128,7 @@
   </nz-form-item>
 
   <div class="flex justify-end pr-8 py-4">
-    <button nz-button class="precaution-button" nzType="primary">提交</button>
-    <button nz-button nzType="primary" class="precaution-secondary ml-4" (click)="onCancel()">取消</button>
+    <button nz-button nzType="primary" class="precaution-secondary" (click)="onCancel()">取消</button>
+    <button nz-button class="precaution-button ml-4" nzType="primary">提交</button>
   </div>
 </form>

+ 1 - 1
src/app/shared/issue-report-form/issue-report-form.component.ts

@@ -131,7 +131,7 @@ export class IssueReportFormComponent {
     return this.basic.departments;
   }
   get users() {
-    return this.setting.usersOfCurrentCompany$;
+    return this.setting.usersOfCurrentCompany;
   }
   get imageUrls() {
     return this.imageUploadGroup.fileList.map(file => file.url);

+ 10 - 6
src/app/shared/scroll-images/scroll-images.component.html

@@ -9,11 +9,15 @@
     </div>
   </div>
   @if (directorVisible) {
-    <div class="left-arrow" (click)="handleScroll('left')">
-      <i nz-icon nzType="left" nzTheme="outline"></i>
-    </div>
-    <div class="right-arrow" (click)="handleScroll('right')">
-      <i nz-icon nzType="right" nzTheme="outline"></i>
-    </div>
+    @if (leftVisible) {
+      <div class="left-arrow" (click)="handleScroll('left')">
+        <i nz-icon nzType="left" nzTheme="outline"></i>
+      </div>
+    }
+    @if (rightVisible) {
+      <div class="right-arrow" (click)="handleScroll('right')">
+        <i nz-icon nzType="right" nzTheme="outline"></i>
+      </div>
+    }
   }
 </div>

+ 11 - 1
src/app/shared/scroll-images/scroll-images.component.ts

@@ -16,6 +16,8 @@ export class ScrollImagesComponent {
   @Input() height = 88;
 
   directorVisible = false;
+  leftVisible = false;
+  rightVisible = false;
 
   constructor(private viewContainerRef: ViewContainerRef) {}
 
@@ -33,6 +35,7 @@ export class ScrollImagesComponent {
     setTimeout(() => {
       if (this.outer.scrollWidth > this.outer.offsetWidth) {
         this.directorVisible = true;
+        this.rightVisible = true;
       }
     });
   }
@@ -40,11 +43,18 @@ export class ScrollImagesComponent {
   handleScroll(direction: 'left' | 'right') {
     const scrollLeft =
       direction === 'left' ? this.outer.scrollLeft - this.width * 3 : this.outer.scrollLeft + this.width * 3;
-    console.log(direction, scrollLeft);
     this.outer.scrollTo({
       left: scrollLeft,
       behavior: 'smooth',
     });
+    setTimeout(() => {
+      this.checkSingleVisible();
+    }, 300);
+  }
+  checkSingleVisible() {
+    const { scrollWidth, scrollLeft, clientWidth } = this.outer;
+    this.rightVisible = clientWidth + scrollLeft < scrollWidth;
+    this.leftVisible = scrollLeft > 0;
   }
 
   get dom() {

+ 1 - 0
src/assets/icons/material-left-arrow.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24"><path d="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6l6 6l1.41-1.41z" fill="currentColor"></path></svg>

+ 1 - 0
src/assets/icons/material-right-arrow.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24"><path d="M8.59 16.59L13.17 12L8.59 7.41L10 6l6 6l-6 6l-1.41-1.41z" fill="currentColor"></path></svg>

+ 19 - 0
src/assets/icons/tool.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title></title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="隐患管理-排查计划" transform="translate(-321.000000, -388.000000)" fill-rule="nonzero">
+            <g id="编组-9" transform="translate(311.000000, 345.000000)">
+                <g id="Frame-192660" transform="translate(10.000000, 11.000000)">
+                    <g id="开发框架" transform="translate(0.000000, 32.000000)">
+                        <rect x="0" y="0" width="20" height="20"></rect>
+                        <circle id="Ellipse-47" fill="currentColor" cx="10" cy="10" r="10"></circle>
+                        <g id="general/tool" transform="translate(4.567468, 5.107148)" fill="#FFFFFF" stroke="#FFFFFF" stroke-width="1.17000008">
+                            <path d="M2.43098229,3.43889062 C2.43269174,2.18350815 3.22880077,1.11264386 4.34309321,0.70252061 C4.35881238,0.696732722 4.37539779,0.708398659 4.37537503,0.725140801 C4.37537503,0.725140801 4.37234886,2.9510783 4.37234886,2.9510783 C4.37216672,3.08505353 4.48063507,3.19351334 4.61461029,3.1933312 C4.61461029,3.1933312 6.07014059,3.1913524 6.07014059,3.1913524 C6.20411582,3.19117026 6.31287927,3.08241513 6.31306141,2.9484399 C6.31306141,2.9484399 6.31608758,0.722502977 6.31608758,0.722502977 C6.31611034,0.705760893 6.33272754,0.694049417 6.34843142,0.699794586 C7.46161429,1.10688933 8.25481294,2.17559296 8.25310899,3.43097543 C8.25138308,4.6984679 7.43986054,5.77786772 6.30866942,6.17902667 C6.30866942,6.17902667 6.30382699,9.74093373 6.30382699,9.74093373 C6.30364483,9.87492602 6.19488147,9.98361452 6.06090624,9.98379666 C6.06090624,9.98379666 4.60537594,9.98577569 4.60537594,9.98577569 C4.47140071,9.9859576 4.36293228,9.87756442 4.36311444,9.74357214 C4.36311444,9.74357214 4.36795687,6.18166508 4.36795687,6.18166508 C3.23785425,5.78358036 2.42926188,4.70638309 2.43098229,3.43889062 C2.43098229,3.43889062 2.43098229,3.43889062 2.43098229,3.43889062 Z" id="Union" transform="translate(5.342046, 5.342046) rotate(45.000000) translate(-5.342046, -5.342046) "></path>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

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

@@ -13,4 +13,15 @@ declare namespace Hazard {
     responsibleDepartments: number[];
     images: string[];
   }
+
+  interface Plan {
+    id: number;
+    type: number;
+    status: number;
+    name: string;
+    startDate: string;
+    endDate: string;
+    createDate: string;
+    department: number;
+  }
 }