employee-records.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. import { Component, OnInit, signal, computed, Inject } from '@angular/core';
  2. import { CommonModule, NgIf, NgFor } from '@angular/common';
  3. import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
  4. import { MatButtonModule } from '@angular/material/button';
  5. import { MatInputModule } from '@angular/material/input';
  6. import { MatSelectModule } from '@angular/material/select';
  7. import { MatDatepickerModule } from '@angular/material/datepicker';
  8. import { MatNativeDateModule } from '@angular/material/core';
  9. import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
  10. import { MatTableModule } from '@angular/material/table';
  11. import { MatPaginatorModule } from '@angular/material/paginator';
  12. import { MatCheckboxModule } from '@angular/material/checkbox';
  13. import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
  14. import { MatIconModule } from '@angular/material/icon';
  15. import { MatTooltipModule } from '@angular/material/tooltip';
  16. import { MatTabsModule } from '@angular/material/tabs';
  17. import { MatChipsModule } from '@angular/material/chips';
  18. import { MatMenuModule } from '@angular/material/menu';
  19. import { MatCardModule } from '@angular/material/card';
  20. import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
  21. import { Router } from '@angular/router';
  22. import { Employee, Department, Position, Contract, Certificate, EmployeeStatus } from '../../../models/hr.model';
  23. // 新增员工对话框组件
  24. @Component({
  25. selector: 'app-add-employee-dialog',
  26. standalone: true,
  27. imports: [
  28. CommonModule,
  29. NgFor,
  30. FormsModule,
  31. ReactiveFormsModule,
  32. MatButtonModule,
  33. MatInputModule,
  34. MatSelectModule,
  35. MatDatepickerModule,
  36. MatNativeDateModule,
  37. MatDialogModule,
  38. MatIconModule
  39. ],
  40. template: `
  41. <h2 mat-dialog-title>{{data.isEdit ? '编辑员工信息' : '新增员工'}}</h2>
  42. <mat-dialog-content>
  43. <form [formGroup]="employeeForm" class="employee-form">
  44. <div class="form-row">
  45. <mat-form-field appearance="outline">
  46. <mat-label>姓名</mat-label>
  47. <input matInput formControlName="name" required>
  48. <mat-error *ngIf="employeeForm.get('name')?.hasError('required')">姓名不能为空</mat-error>
  49. </mat-form-field>
  50. <mat-form-field appearance="outline">
  51. <mat-label>工号</mat-label>
  52. <input matInput formControlName="employeeId" required>
  53. <mat-error *ngIf="employeeForm.get('employeeId')?.hasError('required')">工号不能为空</mat-error>
  54. </mat-form-field>
  55. </div>
  56. <div class="form-row">
  57. <mat-form-field appearance="outline">
  58. <mat-label>部门</mat-label>
  59. <mat-select formControlName="department" required>
  60. <mat-option *ngFor="let dept of departments" [value]="dept.name">{{dept.name}}</mat-option>
  61. </mat-select>
  62. <mat-error *ngIf="employeeForm.get('department')?.hasError('required')">请选择部门</mat-error>
  63. </mat-form-field>
  64. <mat-form-field appearance="outline">
  65. <mat-label>职位</mat-label>
  66. <mat-select formControlName="position" required>
  67. <mat-option *ngFor="let pos of positions" [value]="pos.name">{{pos.name}}</mat-option>
  68. </mat-select>
  69. <mat-error *ngIf="employeeForm.get('position')?.hasError('required')">请选择职位</mat-error>
  70. </mat-form-field>
  71. </div>
  72. <div class="form-row">
  73. <mat-form-field appearance="outline">
  74. <mat-label>手机号码</mat-label>
  75. <input matInput formControlName="phone" required>
  76. <mat-error *ngIf="employeeForm.get('phone')?.hasError('required')">手机号码不能为空</mat-error>
  77. <mat-error *ngIf="employeeForm.get('phone')?.hasError('pattern')">请输入有效的手机号码</mat-error>
  78. </mat-form-field>
  79. <mat-form-field appearance="outline">
  80. <mat-label>邮箱</mat-label>
  81. <input matInput formControlName="email" required>
  82. <mat-error *ngIf="employeeForm.get('email')?.hasError('required')">邮箱不能为空</mat-error>
  83. <mat-error *ngIf="employeeForm.get('email')?.hasError('email')">请输入有效的邮箱地址</mat-error>
  84. </mat-form-field>
  85. </div>
  86. <div class="form-row">
  87. <mat-form-field appearance="outline">
  88. <mat-label>性别</mat-label>
  89. <mat-select formControlName="gender" required>
  90. <mat-option value="男">男</mat-option>
  91. <mat-option value="女">女</mat-option>
  92. </mat-select>
  93. <mat-error *ngIf="employeeForm.get('gender')?.hasError('required')">请选择性别</mat-error>
  94. </mat-form-field>
  95. <mat-form-field appearance="outline">
  96. <mat-label>出生日期</mat-label>
  97. <input matInput [matDatepicker]="birthDatePicker" formControlName="birthDate">
  98. <mat-datepicker-toggle matSuffix [for]="birthDatePicker"></mat-datepicker-toggle>
  99. <mat-datepicker #birthDatePicker></mat-datepicker>
  100. </mat-form-field>
  101. </div>
  102. <div class="form-row">
  103. <mat-form-field appearance="outline">
  104. <mat-label>入职日期</mat-label>
  105. <input matInput [matDatepicker]="hireDatePicker" formControlName="hireDate" required>
  106. <mat-datepicker-toggle matSuffix [for]="hireDatePicker"></mat-datepicker-toggle>
  107. <mat-datepicker #hireDatePicker></mat-datepicker>
  108. <mat-error *ngIf="employeeForm.get('hireDate')?.hasError('required')">入职日期不能为空</mat-error>
  109. </mat-form-field>
  110. <mat-form-field appearance="outline">
  111. <mat-label>员工状态</mat-label>
  112. <mat-select formControlName="status" required>
  113. <mat-option value="在职">在职</mat-option>
  114. <mat-option value="离职">离职</mat-option>
  115. <mat-option value="试用期">试用期</mat-option>
  116. </mat-select>
  117. <mat-error *ngIf="employeeForm.get('status')?.hasError('required')">请选择员工状态</mat-error>
  118. </mat-form-field>
  119. </div>
  120. </form>
  121. </mat-dialog-content>
  122. <mat-dialog-actions align="end">
  123. <button mat-button mat-dialog-close>取消</button>
  124. <button mat-raised-button color="primary" [disabled]="employeeForm.invalid" (click)="saveEmployee()">保存</button>
  125. </mat-dialog-actions>
  126. `,
  127. styles: [`
  128. .employee-form {
  129. display: flex;
  130. flex-direction: column;
  131. gap: 16px;
  132. padding: 8px;
  133. }
  134. .form-row {
  135. display: flex;
  136. gap: 16px;
  137. mat-form-field {
  138. flex: 1;
  139. }
  140. }
  141. `]
  142. })
  143. export class AddEmployeeDialog implements OnInit {
  144. employeeForm: FormGroup;
  145. departments: Department[] = [
  146. { id: '1', name: '设计部', employeeCount: 0 },
  147. { id: '2', name: '客服部', employeeCount: 0 },
  148. { id: '3', name: '技术部', employeeCount: 0 },
  149. { id: '4', name: '行政部', employeeCount: 0 }
  150. ];
  151. positions: Position[] = [
  152. { id: '1', name: '设计师', departmentId: '1', departmentName: '设计部', level: '中级' },
  153. { id: '2', name: '客服专员', departmentId: '2', departmentName: '客服部', level: '初级' },
  154. { id: '3', name: '技术组长', departmentId: '3', departmentName: '技术部', level: '高级' },
  155. { id: '4', name: '前端开发', departmentId: '3', departmentName: '技术部', level: '中级' },
  156. { id: '5', name: '行政专员', departmentId: '4', departmentName: '行政部', level: '初级' }
  157. ];
  158. constructor(
  159. private fb: FormBuilder,
  160. public dialogRef: MatDialogRef<AddEmployeeDialog>,
  161. @Inject(MAT_DIALOG_DATA) public data: {employee?: Employee, isEdit: boolean}
  162. ) {
  163. this.employeeForm = this.fb.group({
  164. name: ['', Validators.required],
  165. employeeId: ['', Validators.required],
  166. department: ['', Validators.required],
  167. position: ['', Validators.required],
  168. phone: ['', [Validators.required, Validators.pattern(/^1[3-9]\d{9}$/)]],
  169. email: ['', [Validators.required, Validators.email]],
  170. gender: ['', Validators.required],
  171. birthDate: [null],
  172. hireDate: [new Date(), Validators.required],
  173. status: ['试用期', Validators.required]
  174. });
  175. if (data.isEdit && data.employee) {
  176. this.employeeForm.patchValue(data.employee);
  177. }
  178. }
  179. ngOnInit() {}
  180. saveEmployee() {
  181. if (this.employeeForm.valid) {
  182. const employeeData = this.employeeForm.value;
  183. this.dialogRef.close(employeeData);
  184. }
  185. }
  186. }
  187. // 入职流程对话框组件
  188. @Component({
  189. selector: 'app-onboarding-dialog',
  190. standalone: true,
  191. imports: [
  192. CommonModule,
  193. NgFor,
  194. FormsModule,
  195. ReactiveFormsModule,
  196. MatButtonModule,
  197. MatInputModule,
  198. MatSelectModule,
  199. MatDatepickerModule,
  200. MatNativeDateModule,
  201. MatDialogModule,
  202. MatIconModule,
  203. MatCheckboxModule
  204. ],
  205. template: `
  206. <h2 mat-dialog-title>入职流程</h2>
  207. <mat-dialog-content>
  208. <div class="employee-info">
  209. <h3>员工信息</h3>
  210. <p><strong>姓名:</strong> {{data.employee.name}}</p>
  211. <p><strong>部门:</strong> {{data.employee.department}}</p>
  212. <p><strong>职位:</strong> {{data.employee.position}}</p>
  213. <p><strong>入职日期:</strong> {{data.employee.hireDate | date:'yyyy-MM-dd'}}</p>
  214. </div>
  215. <div class="onboarding-tasks">
  216. <h3>入职任务清单</h3>
  217. <div class="task-list">
  218. <div *ngFor="let task of onboardingTasks" class="task-item">
  219. <mat-checkbox [(ngModel)]="task.completed">{{task.name}}</mat-checkbox>
  220. <span class="task-assignee">负责人: {{task.assignee}}</span>
  221. </div>
  222. </div>
  223. </div>
  224. </mat-dialog-content>
  225. <mat-dialog-actions align="end">
  226. <button mat-button mat-dialog-close>关闭</button>
  227. <button mat-raised-button color="primary" (click)="saveOnboarding()">保存进度</button>
  228. </mat-dialog-actions>
  229. `,
  230. styles: [`
  231. .employee-info {
  232. margin-bottom: 24px;
  233. padding: 16px;
  234. background-color: #f5f5f5;
  235. border-radius: 4px;
  236. h3 {
  237. margin-top: 0;
  238. margin-bottom: 16px;
  239. color: #1a3a6e;
  240. }
  241. p {
  242. margin: 8px 0;
  243. }
  244. }
  245. .onboarding-tasks {
  246. h3 {
  247. margin-bottom: 16px;
  248. color: #1a3a6e;
  249. }
  250. }
  251. .task-list {
  252. display: flex;
  253. flex-direction: column;
  254. gap: 12px;
  255. }
  256. .task-item {
  257. display: flex;
  258. justify-content: space-between;
  259. align-items: center;
  260. padding: 8px 0;
  261. border-bottom: 1px solid #e0e0e0;
  262. &:last-child {
  263. border-bottom: none;
  264. }
  265. .task-assignee {
  266. color: #757575;
  267. font-size: 12px;
  268. }
  269. }
  270. `]
  271. })
  272. export class OnboardingDialog {
  273. onboardingTasks = [
  274. { name: '签署劳动合同', completed: false, assignee: '人事' },
  275. { name: '录入系统权限', completed: false, assignee: '技术支持' },
  276. { name: '领取办公用品', completed: false, assignee: '行政' },
  277. { name: '参加新员工培训', completed: false, assignee: '培训部' },
  278. { name: '技术组长审核岗位匹配度', completed: false, assignee: '技术组长' }
  279. ];
  280. constructor(
  281. public dialogRef: MatDialogRef<OnboardingDialog>,
  282. @Inject(MAT_DIALOG_DATA) public data: {employee: Employee}
  283. ) {}
  284. saveOnboarding() {
  285. this.dialogRef.close(this.onboardingTasks);
  286. }
  287. }
  288. // 离职流程对话框组件
  289. @Component({
  290. selector: 'app-offboarding-dialog',
  291. standalone: true,
  292. imports: [
  293. CommonModule,
  294. NgFor,
  295. FormsModule,
  296. ReactiveFormsModule,
  297. MatButtonModule,
  298. MatInputModule,
  299. MatSelectModule,
  300. MatDatepickerModule,
  301. MatNativeDateModule,
  302. MatDialogModule,
  303. MatIconModule,
  304. MatCheckboxModule
  305. ],
  306. template: `
  307. <h2 mat-dialog-title>离职流程</h2>
  308. <mat-dialog-content>
  309. <form [formGroup]="offboardingForm" class="offboarding-form">
  310. <div class="employee-info">
  311. <h3>员工信息</h3>
  312. <p><strong>姓名:</strong> {{data.employee.name}}</p>
  313. <p><strong>部门:</strong> {{data.employee.department}}</p>
  314. <p><strong>职位:</strong> {{data.employee.position}}</p>
  315. <p><strong>入职日期:</strong> {{data.employee.hireDate | date:'yyyy-MM-dd'}}</p>
  316. </div>
  317. <div class="offboarding-details">
  318. <h3>离职信息</h3>
  319. <mat-form-field appearance="outline" class="full-width">
  320. <mat-label>离职原因</mat-label>
  321. <mat-select formControlName="reason" required>
  322. <mat-option value="个人发展">个人发展</mat-option>
  323. <mat-option value="薪资问题">薪资问题</mat-option>
  324. <mat-option value="工作环境">工作环境</mat-option>
  325. <mat-option value="家庭原因">家庭原因</mat-option>
  326. <mat-option value="健康原因">健康原因</mat-option>
  327. <mat-option value="其他">其他</mat-option>
  328. </mat-select>
  329. <mat-error *ngIf="offboardingForm.get('reason')?.hasError('required')">请选择离职原因</mat-error>
  330. </mat-form-field>
  331. <mat-form-field appearance="outline" class="full-width">
  332. <mat-label>离职日期</mat-label>
  333. <input matInput [matDatepicker]="resignDatePicker" formControlName="resignDate" required>
  334. <mat-datepicker-toggle matSuffix [for]="resignDatePicker"></mat-datepicker-toggle>
  335. <mat-datepicker #resignDatePicker></mat-datepicker>
  336. <mat-error *ngIf="offboardingForm.get('resignDate')?.hasError('required')">离职日期不能为空</mat-error>
  337. </mat-form-field>
  338. <mat-form-field appearance="outline" class="full-width">
  339. <mat-label>工作交接要求</mat-label>
  340. <textarea matInput formControlName="handoverRequirements" rows="3"></textarea>
  341. </mat-form-field>
  342. </div>
  343. <div class="offboarding-tasks">
  344. <h3>离职任务清单</h3>
  345. <div class="task-list">
  346. <div *ngFor="let task of offboardingTasks" class="task-item">
  347. <mat-checkbox [(ngModel)]="task.completed" [ngModelOptions]="{standalone: true}">
  348. {{task.name}}
  349. </mat-checkbox>
  350. <span class="task-assignee">负责人: {{task.assignee}}</span>
  351. </div>
  352. </div>
  353. </div>
  354. </form>
  355. </mat-dialog-content>
  356. <mat-dialog-actions align="end">
  357. <button mat-button mat-dialog-close>取消</button>
  358. <button mat-raised-button color="primary" [disabled]="offboardingForm.invalid" (click)="saveOffboarding()">提交</button>
  359. </mat-dialog-actions>
  360. `,
  361. styles: [`
  362. .offboarding-form {
  363. display: flex;
  364. flex-direction: column;
  365. gap: 24px;
  366. }
  367. .employee-info {
  368. padding: 16px;
  369. background-color: #f5f5f5;
  370. border-radius: 4px;
  371. h3 {
  372. margin-top: 0;
  373. margin-bottom: 16px;
  374. color: #1a3a6e;
  375. }
  376. p {
  377. margin: 8px 0;
  378. }
  379. }
  380. .offboarding-details,
  381. .offboarding-tasks {
  382. h3 {
  383. margin-bottom: 16px;
  384. color: #1a3a6e;
  385. }
  386. }
  387. .full-width {
  388. width: 100%;
  389. }
  390. .task-list {
  391. display: flex;
  392. flex-direction: column;
  393. gap: 12px;
  394. }
  395. .task-item {
  396. display: flex;
  397. justify-content: space-between;
  398. align-items: center;
  399. padding: 8px 0;
  400. border-bottom: 1px solid #e0e0e0;
  401. &:last-child {
  402. border-bottom: none;
  403. }
  404. .task-assignee {
  405. color: #757575;
  406. font-size: 12px;
  407. }
  408. }
  409. `]
  410. })
  411. export class OffboardingDialog implements OnInit {
  412. offboardingForm: FormGroup;
  413. offboardingTasks = [
  414. { name: '项目交接', completed: false, assignee: '技术组长' },
  415. { name: '客户信息同步', completed: false, assignee: '客服主管' },
  416. { name: '薪资结算', completed: false, assignee: '财务' },
  417. { name: '归还办公设备', completed: false, assignee: '行政' },
  418. { name: '系统权限冻结', completed: false, assignee: '技术支持' }
  419. ];
  420. constructor(
  421. private fb: FormBuilder,
  422. public dialogRef: MatDialogRef<OffboardingDialog>,
  423. @Inject(MAT_DIALOG_DATA) public data: {employee: Employee}
  424. ) {
  425. this.offboardingForm = this.fb.group({
  426. reason: ['', Validators.required],
  427. resignDate: [new Date(), Validators.required],
  428. handoverRequirements: ['']
  429. });
  430. }
  431. ngOnInit() {}
  432. saveOffboarding() {
  433. if (this.offboardingForm.valid) {
  434. const offboardingData = {
  435. ...this.offboardingForm.value,
  436. tasks: this.offboardingTasks
  437. };
  438. this.dialogRef.close(offboardingData);
  439. }
  440. }
  441. }
  442. @Component({
  443. selector: 'app-employee-records',
  444. standalone: true,
  445. imports: [
  446. CommonModule,
  447. NgIf,
  448. NgFor,
  449. FormsModule,
  450. ReactiveFormsModule,
  451. MatButtonModule,
  452. MatInputModule,
  453. MatSelectModule,
  454. MatDatepickerModule,
  455. MatNativeDateModule,
  456. MatDialogModule,
  457. MatTableModule,
  458. MatPaginatorModule,
  459. MatCheckboxModule,
  460. MatSnackBarModule,
  461. MatIconModule,
  462. MatTooltipModule,
  463. MatTabsModule,
  464. MatChipsModule,
  465. MatMenuModule,
  466. MatCardModule,
  467. MatProgressSpinnerModule
  468. ],
  469. templateUrl: './employee-records.html',
  470. styleUrls: ['./employee-records.scss']
  471. })
  472. export class EmployeeRecords implements OnInit {
  473. // 员工数据
  474. employees = signal<Employee[]>([
  475. {
  476. id: '1',
  477. name: '张三',
  478. department: '设计部',
  479. position: '设计师',
  480. employeeId: 'DS001',
  481. phone: '13800138001',
  482. email: 'zhangsan@example.com',
  483. gender: '男',
  484. birthDate: new Date('1990-01-01'),
  485. hireDate: new Date('2022-01-15'),
  486. status: '在职',
  487. idCard: '110105199001011234',
  488. bankCard: '6222021234567890'
  489. },
  490. {
  491. id: '2',
  492. name: '李四',
  493. department: '客服部',
  494. position: '客服专员',
  495. employeeId: 'CS001',
  496. phone: '13800138002',
  497. email: 'lisi@example.com',
  498. gender: '女',
  499. birthDate: new Date('1992-05-20'),
  500. hireDate: new Date('2022-03-10'),
  501. status: '在职',
  502. idCard: '110105199205201256',
  503. bankCard: '6216619876543210'
  504. },
  505. {
  506. id: '3',
  507. name: '王五',
  508. department: '技术部',
  509. position: '前端开发',
  510. employeeId: 'FE001',
  511. phone: '13800138003',
  512. email: 'wangwu@example.com',
  513. gender: '男',
  514. birthDate: new Date('1988-11-15'),
  515. hireDate: new Date('2023-01-05'),
  516. status: '试用期',
  517. idCard: '110105198811151278',
  518. bankCard: '6225880011223344'
  519. },
  520. {
  521. id: '4',
  522. name: '赵六',
  523. department: '设计部',
  524. position: '高级设计师',
  525. employeeId: 'DS002',
  526. phone: '13800138004',
  527. email: 'zhaoliu@example.com',
  528. gender: '男',
  529. birthDate: new Date('1985-07-22'),
  530. hireDate: new Date('2021-06-18'),
  531. status: '在职',
  532. idCard: '110105198507221299',
  533. bankCard: '6217009988776655'
  534. },
  535. {
  536. id: '5',
  537. name: '钱七',
  538. department: '技术部',
  539. position: '技术组长',
  540. employeeId: 'TL001',
  541. phone: '13800138005',
  542. email: 'qianqi@example.com',
  543. gender: '男',
  544. birthDate: new Date('1983-03-30'),
  545. hireDate: new Date('2020-09-01'),
  546. status: '在职',
  547. idCard: '110105198303301233',
  548. bankCard: '6222035566778899'
  549. }
  550. ]);
  551. // 筛选条件
  552. filterName = signal('');
  553. filterDepartment = signal('');
  554. filterPosition = signal('');
  555. filterStatus = signal('');
  556. // 表格列定义
  557. displayedColumns = ['select', 'name', 'employeeId', 'department', 'position', 'phone', 'idCard', 'bankCard', 'hireDate', 'status', 'actions'];
  558. // 选中的员工
  559. selectedEmployees = signal<string[]>([]);
  560. // 展示敏感信息的行(按员工id)
  561. sensitiveExpandedIds = signal<string[]>([]);
  562. // 是否有任意行展开敏感信息
  563. isAnySensitiveExpanded = computed(() => this.sensitiveExpandedIds().length > 0);
  564. // 部门和职位数据
  565. departments = [
  566. { id: '1', name: '设计部', employeeCount: 25 },
  567. { id: '2', name: '客服部', employeeCount: 18 },
  568. { id: '3', name: '技术部', employeeCount: 30 },
  569. { id: '4', name: '行政部', employeeCount: 10 }
  570. ];
  571. positions = [
  572. { id: '1', name: '设计师', departmentId: '1', departmentName: '设计部', level: '中级' },
  573. { id: '2', name: '高级设计师', departmentId: '1', departmentName: '设计部', level: '高级' },
  574. { id: '3', name: '客服专员', departmentId: '2', departmentName: '客服部', level: '初级' },
  575. { id: '4', name: '客服主管', departmentId: '2', departmentName: '客服部', level: '高级' },
  576. { id: '5', name: '前端开发', departmentId: '3', departmentName: '技术部', level: '中级' },
  577. { id: '6', name: '技术组长', departmentId: '3', departmentName: '技术部', level: '高级' },
  578. { id: '7', name: '行政专员', departmentId: '4', departmentName: '行政部', level: '初级' }
  579. ];
  580. // 计算过滤后的员工列表
  581. filteredEmployees = computed(() => {
  582. return this.employees().filter(employee => {
  583. const nameMatch = this.filterName() === '' || employee.name.includes(this.filterName());
  584. const departmentMatch = this.filterDepartment() === '' || employee.department === this.filterDepartment();
  585. const positionMatch = this.filterPosition() === '' || employee.position === this.filterPosition();
  586. const statusMatch = this.filterStatus() === '' || employee.status === this.filterStatus();
  587. return nameMatch && departmentMatch && positionMatch && statusMatch;
  588. });
  589. });
  590. constructor(
  591. private dialog: MatDialog,
  592. private snackBar: MatSnackBar,
  593. private router: Router
  594. ) {}
  595. ngOnInit() {}
  596. // 掩码与格式化工具
  597. maskIdCard(id: string): string {
  598. if (!id) return '';
  599. if (id.length >= 18) return `${id.slice(0, 6)}********${id.slice(-4)}`;
  600. if (id.length > 6) return `${id.slice(0, 3)}****${id.slice(-2)}`;
  601. return id;
  602. }
  603. maskBankCard(card: string): string {
  604. if (!card) return '';
  605. const compact = card.replace(/\s+/g, '');
  606. if (compact.length <= 8) return compact;
  607. const first4 = compact.slice(0, 4);
  608. const last4 = compact.slice(-4);
  609. return `${first4} **** **** ${last4}`;
  610. }
  611. formatBankCard(card: string): string {
  612. if (!card) return '';
  613. return card.replace(/\s+/g, '').replace(/(\d{4})(?=\d)/g, '$1 ').trim();
  614. }
  615. isSensitiveExpanded(id: string): boolean {
  616. return this.sensitiveExpandedIds().includes(id);
  617. }
  618. toggleSensitive(id: string) {
  619. const list = this.sensitiveExpandedIds();
  620. if (list.includes(id)) {
  621. this.sensitiveExpandedIds.set(list.filter(x => x !== id));
  622. } else {
  623. this.sensitiveExpandedIds.set([...list, id]);
  624. }
  625. }
  626. // 打开新增员工对话框
  627. openAddEmployeeDialog() {
  628. const dialogRef = this.dialog.open(AddEmployeeDialog, {
  629. width: '700px',
  630. panelClass: 'hr-dialog',
  631. data: { isEdit: false }
  632. });
  633. dialogRef.afterClosed().subscribe(result => {
  634. if (result) {
  635. const newEmployee: Employee = {
  636. id: (this.employees().length + 1).toString(),
  637. ...result
  638. };
  639. this.employees.update(employees => [...employees, newEmployee]);
  640. this.showSnackBar('员工添加成功');
  641. }
  642. });
  643. }
  644. // 打开编辑员工对话框
  645. openEditEmployeeDialog(employee: Employee) {
  646. const dialogRef = this.dialog.open(AddEmployeeDialog, {
  647. width: '700px',
  648. panelClass: 'hr-dialog',
  649. data: { employee, isEdit: true }
  650. });
  651. dialogRef.afterClosed().subscribe(result => {
  652. if (result) {
  653. this.employees.update(employees =>
  654. employees.map(emp => emp.id === employee.id ? { ...emp, ...result } : emp)
  655. );
  656. this.showSnackBar('员工信息更新成功');
  657. }
  658. });
  659. }
  660. // 打开入职流程对话框
  661. openOnboardingDialog(employee: Employee) {
  662. const dialogRef = this.dialog.open(OnboardingDialog, {
  663. width: '600px',
  664. panelClass: 'hr-dialog',
  665. data: { employee }
  666. });
  667. dialogRef.afterClosed().subscribe(result => {
  668. if (result) {
  669. this.showSnackBar('入职流程更新成功');
  670. }
  671. });
  672. }
  673. // 打开离职流程对话框
  674. openOffboardingDialog(employee: Employee) {
  675. const dialogRef = this.dialog.open(OffboardingDialog, {
  676. width: '600px',
  677. panelClass: 'hr-dialog',
  678. data: { employee }
  679. });
  680. dialogRef.afterClosed().subscribe(result => {
  681. if (result) {
  682. // 更新员工状态为离职
  683. this.employees.update(employees =>
  684. employees.map(emp => emp.id === employee.id ? { ...emp, status: '离职' } : emp)
  685. );
  686. this.showSnackBar('离职流程已启动');
  687. }
  688. });
  689. }
  690. // 删除员工
  691. deleteEmployee(employee: Employee) {
  692. if (confirm(`确定要删除员工 ${employee.name} 吗?`)) {
  693. this.employees.update(employees =>
  694. employees.filter(emp => emp.id !== employee.id)
  695. );
  696. this.showSnackBar('员工已删除');
  697. }
  698. }
  699. // 批量删除员工
  700. batchDelete() {
  701. if (this.selectedEmployees().length === 0) {
  702. this.showSnackBar('请先选择要删除的员工');
  703. return;
  704. }
  705. if (confirm(`确定要删除选中的 ${this.selectedEmployees().length} 名员工吗?`)) {
  706. this.employees.update(employees =>
  707. employees.filter(emp => !this.selectedEmployees().includes(emp.id))
  708. );
  709. this.selectedEmployees.set([]);
  710. this.showSnackBar('批量删除成功');
  711. }
  712. }
  713. // 导出员工数据
  714. exportEmployees(type: string) {
  715. // 实际项目中这里会调用导出服务
  716. this.showSnackBar(`已导出${type}`);
  717. }
  718. // 快捷卡片查看详情
  719. openQuickAction(type: 'onboarding' | 'offboarding' | 'probation') {
  720. switch (type) {
  721. case 'onboarding':
  722. // 这里可以导航或弹窗,先给出提示
  723. this.showSnackBar('前往入职流程列表');
  724. break;
  725. case 'offboarding':
  726. this.showSnackBar('前往离职流程列表');
  727. break;
  728. case 'probation':
  729. this.showSnackBar('查看试用期即将到期员工');
  730. break;
  731. }
  732. }
  733. // 进入员工详情(根据角色跳转至相应详情页)
  734. goToDetails(employee: Employee) {
  735. // 目前仅实现设计师角色详情,后续可按职位扩展其它角色
  736. this.showSnackBar('该角色详情页正在建设中');
  737. }
  738. // 选择/取消选择单个员工
  739. toggleSelection(employeeId: string) {
  740. if (this.isSelected(employeeId)) {
  741. this.selectedEmployees.update(selected => selected.filter(id => id !== employeeId));
  742. } else {
  743. this.selectedEmployees.update(selected => [...selected, employeeId]);
  744. }
  745. }
  746. // 选择/取消选择所有员工
  747. toggleSelectAll(checked: boolean) {
  748. if (checked) {
  749. const allIds = this.filteredEmployees().map(emp => emp.id);
  750. this.selectedEmployees.set(allIds);
  751. } else {
  752. this.selectedEmployees.set([]);
  753. }
  754. }
  755. // 检查员工是否被选中
  756. isSelected(employeeId: string): boolean {
  757. return this.selectedEmployees().includes(employeeId);
  758. }
  759. // 检查是否所有员工都被选中
  760. isAllSelected(): boolean {
  761. return this.filteredEmployees().length > 0 &&
  762. this.filteredEmployees().every(emp => this.selectedEmployees().includes(emp.id));
  763. }
  764. // 显示提示消息
  765. showSnackBar(message: string) {
  766. this.snackBar.open(message, '关闭', {
  767. duration: 3000,
  768. horizontalPosition: 'center',
  769. verticalPosition: 'bottom',
  770. panelClass: ['success-snackbar']
  771. });
  772. }
  773. // 重置筛选条件
  774. resetFilters() {
  775. this.filterName.set('');
  776. this.filterDepartment.set('');
  777. this.filterPosition.set('');
  778. this.filterStatus.set('');
  779. }
  780. }