employee-detail-panel.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, ChangeDetectorRef } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { Router } from '@angular/router';
  4. import { DesignerCalendarComponent, Designer as CalendarDesigner } from '../../customer-service/consultation-order/components/designer-calendar/designer-calendar.component';
  5. import { ProjectProgressModalComponent } from '../project-timeline/project-progress-modal';
  6. import { ProjectSpaceDeliverableService, ProjectSpaceDeliverableSummary } from '../../../../modules/project/services/project-space-deliverable.service';
  7. import { normalizeDateInput, addDays } from '../../../utils/date-utils';
  8. // 员工详情面板数据接口
  9. export interface EmployeeDetail {
  10. name: string;
  11. currentProjects: number; // 当前负责项目数
  12. projectNames: string[]; // 项目名称列表(用于显示)
  13. projectData: Array<{ id: string; name: string }>; // 项目数据(包含ID和名称,用于跳转)
  14. leaveRecords: LeaveRecord[]; // 未来7天请假记录
  15. redMarkExplanation: string; // 红色标记说明
  16. calendarData?: EmployeeCalendarData; // 负载日历数据
  17. // 问卷相关
  18. surveyCompleted?: boolean; // 是否完成问卷
  19. surveyData?: any; // 问卷答案数据
  20. profileId?: string; // Profile ID
  21. }
  22. // 请假记录接口
  23. export interface LeaveRecord {
  24. id: string;
  25. employeeName: string;
  26. date: string; // YYYY-MM-DD 格式
  27. isLeave: boolean;
  28. leaveType?: 'sick' | 'personal' | 'annual' | 'other'; // 请假类型
  29. reason?: string; // 请假原因
  30. }
  31. // 员工日历数据接口
  32. export interface EmployeeCalendarData {
  33. currentMonth: Date;
  34. days: EmployeeCalendarDay[];
  35. }
  36. // 日历日期数据
  37. export interface EmployeeCalendarDay {
  38. date: Date;
  39. projectCount: number; // 当天项目数量
  40. projects: Array<{ id: string; name: string; deadline?: Date }>; // 项目列表
  41. isToday: boolean;
  42. isCurrentMonth: boolean;
  43. }
  44. @Component({
  45. selector: 'app-employee-detail-panel',
  46. standalone: true,
  47. imports: [CommonModule, DesignerCalendarComponent, ProjectProgressModalComponent],
  48. templateUrl: './employee-detail-panel.html',
  49. styleUrls: ['./employee-detail-panel.scss']
  50. })
  51. export class EmployeeDetailPanelComponent implements OnInit, OnChanges {
  52. // 暴露 Array 给模板使用
  53. Array = Array;
  54. // 输入属性
  55. @Input() visible: boolean = false;
  56. // 兼容旧模式:直接传入详情数据
  57. @Input() employeeDetail: EmployeeDetail | null = null;
  58. // 🆕 替换直接传入 employeeDetail,改为传入基础数据自行计算
  59. @Input() employeeName: string = '';
  60. @Input() projects: any[] = []; // 该员工的项目列表
  61. @Input() allLeaveRecords: LeaveRecord[] = []; // 所有请假记录(组件内过滤)
  62. @Input() embedMode: boolean = false; // 嵌入模式
  63. // 输出事件
  64. @Output() close = new EventEmitter<void>();
  65. @Output() projectClick = new EventEmitter<string>();
  66. // 兼容旧模式的输出事件
  67. @Output() calendarMonthChange = new EventEmitter<number>();
  68. @Output() calendarDayClick = new EventEmitter<EmployeeCalendarDay>();
  69. @Output() refreshSurvey = new EventEmitter<void>();
  70. // 组件内部状态
  71. internalEmployeeDetail: EmployeeDetail | null = null; // 内部生成的详情
  72. showFullSurvey: boolean = false;
  73. refreshingSurvey: boolean = false;
  74. // 获取负载状态
  75. get workloadStatus(): { label: string; class: string; color: string } {
  76. const count = this.currentEmployeeDetail?.currentProjects || 0;
  77. if (count >= 3) {
  78. return { label: '高负载', class: 'high', color: '#ef4444' }; // red
  79. } else if (count >= 1) {
  80. return { label: '正常', class: 'normal', color: '#3b82f6' }; // blue
  81. }
  82. return { label: '空闲', class: 'low', color: '#10b981' }; // green
  83. }
  84. // 获取能力标签
  85. get strengthTags(): string[] {
  86. if (!this.currentEmployeeDetail?.surveyData?.answers) return [];
  87. const summary = this.getCapabilitySummary(this.currentEmployeeDetail.surveyData.answers);
  88. const tags: string[] = [];
  89. // 解析风格
  90. if (summary.styles && summary.styles !== '未填写') {
  91. tags.push(...summary.styles.split('、').slice(0, 3));
  92. }
  93. // 解析空间
  94. if (summary.spaces && summary.spaces !== '未填写') {
  95. tags.push(...summary.spaces.split('、').slice(0, 2));
  96. }
  97. // 技术优势
  98. if (summary.advantages && summary.advantages !== '未填写') {
  99. tags.push(...summary.advantages.split('、').slice(0, 2));
  100. }
  101. return tags.slice(0, 6); // 最多显示6个标签
  102. }
  103. // 获取当前显示的 employeeDetail(优先使用 Input,否则使用内部生成的)
  104. get currentEmployeeDetail(): EmployeeDetail | null {
  105. return this.employeeDetail || this.internalEmployeeDetail;
  106. }
  107. // 日历项目列表弹窗状态
  108. showCalendarProjectList: boolean = false;
  109. selectedDate: Date | null = null;
  110. selectedDayProjects: Array<{ id: string; name: string; deadline?: Date }> = [];
  111. // 设计师详细日历
  112. showDesignerCalendar: boolean = false;
  113. calendarDesigners: CalendarDesigner[] = [];
  114. calendarViewMode: 'week' | 'month' | 'quarter' = 'month';
  115. // 项目进度详情弹窗
  116. showProgressModal: boolean = false;
  117. selectedProjectForProgress: string = '';
  118. progressSummary: ProjectSpaceDeliverableSummary | null = null;
  119. loadingProgress: boolean = false;
  120. constructor(
  121. private router: Router,
  122. private cdr: ChangeDetectorRef,
  123. private projectSpaceDeliverableService: ProjectSpaceDeliverableService
  124. ) {}
  125. ngOnInit(): void {
  126. console.log('📋 EmployeeDetailPanelComponent 初始化');
  127. }
  128. ngOnChanges(changes: SimpleChanges): void {
  129. // 优先处理直接传入的 employeeDetail
  130. if (changes['employeeDetail']) {
  131. // 如果外部传入了数据,不需要做额外操作,直接使用
  132. return;
  133. }
  134. // 当 visible 变为 true,或员工姓名/项目数据改变时,重新生成详情(仅在未传入 employeeDetail 时)
  135. if (!this.employeeDetail && (
  136. (changes['visible']?.currentValue === true) ||
  137. (this.visible && (changes['employeeName'] || changes['projects']))
  138. )) {
  139. if (this.employeeName) {
  140. this.generateEmployeeDetail();
  141. }
  142. }
  143. }
  144. /**
  145. * 生成员工详情数据
  146. */
  147. async generateEmployeeDetail(): Promise<void> {
  148. if (!this.employeeName) return;
  149. const employeeName = this.employeeName;
  150. // 过滤出当前活跃的项目(非已完成/已交付)用于显示"当前项目数"
  151. const activeProjects = this.projects.filter(p => p.status !== '已完成' && p.status !== '已交付');
  152. const currentProjects = activeProjects.length;
  153. // 保存完整的项目数据(最多显示3个活跃项目)
  154. const projectData = activeProjects.slice(0, 3).map(p => ({
  155. id: p.id,
  156. name: p.name
  157. }));
  158. const projectNames = projectData.map(p => p.name); // 项目名称列表
  159. // 获取该员工的请假记录(未来7天)
  160. const today = new Date();
  161. const next7Days = Array.from({ length: 7 }, (_, i) => {
  162. const date = new Date(today);
  163. date.setDate(today.getDate() + i);
  164. return date.toISOString().split('T')[0]; // YYYY-MM-DD 格式
  165. });
  166. const employeeLeaveRecords = this.allLeaveRecords.filter(record =>
  167. record.employeeName === employeeName && next7Days.includes(record.date)
  168. );
  169. // 生成红色标记说明
  170. const redMarkExplanation = this.generateRedMarkExplanation(employeeName, employeeLeaveRecords, currentProjects);
  171. // 生成日历数据 (传入所有项目以显示历史负载)
  172. const calendarData = this.generateEmployeeCalendar(employeeName, this.projects);
  173. // 构建基础对象
  174. this.internalEmployeeDetail = {
  175. name: employeeName,
  176. currentProjects,
  177. projectNames,
  178. projectData,
  179. leaveRecords: employeeLeaveRecords,
  180. redMarkExplanation,
  181. calendarData
  182. };
  183. // 加载问卷数据
  184. await this.loadSurveyData(employeeName);
  185. // 触发变更检测
  186. this.cdr.markForCheck();
  187. }
  188. /**
  189. * 加载问卷数据
  190. */
  191. async loadSurveyData(employeeName: string): Promise<void> {
  192. if (!this.internalEmployeeDetail) return;
  193. try {
  194. const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
  195. // 通过员工名字查找Profile(同时查询 realname 和 name 字段)
  196. const realnameQuery = new Parse.Query('Profile');
  197. realnameQuery.equalTo('realname', employeeName);
  198. const nameQuery = new Parse.Query('Profile');
  199. nameQuery.equalTo('name', employeeName);
  200. // 使用 or 查询
  201. const profileQuery = Parse.Query.or(realnameQuery, nameQuery);
  202. profileQuery.limit(1);
  203. const profileResults = await profileQuery.find();
  204. if (profileResults.length > 0) {
  205. const profile = profileResults[0];
  206. this.internalEmployeeDetail.profileId = profile.id;
  207. this.internalEmployeeDetail.surveyCompleted = profile.get('surveyCompleted') || false;
  208. // 如果已完成问卷,加载问卷答案
  209. if (this.internalEmployeeDetail.surveyCompleted) {
  210. const surveyQuery = new Parse.Query('SurveyLog');
  211. surveyQuery.equalTo('profile', profile.toPointer());
  212. surveyQuery.equalTo('type', 'survey-profile');
  213. surveyQuery.descending('createdAt');
  214. surveyQuery.limit(1);
  215. const surveyResults = await surveyQuery.find();
  216. if (surveyResults.length > 0) {
  217. const survey = surveyResults[0];
  218. this.internalEmployeeDetail.surveyData = {
  219. answers: survey.get('answers') || [],
  220. createdAt: survey.get('createdAt'),
  221. updatedAt: survey.get('updatedAt')
  222. };
  223. }
  224. }
  225. }
  226. this.cdr.markForCheck();
  227. } catch (error) {
  228. console.error(`❌ 加载员工 ${employeeName} 问卷数据失败:`, error);
  229. }
  230. }
  231. /**
  232. * 生成员工日历数据(支持指定月份)
  233. */
  234. private generateEmployeeCalendar(employeeName: string, employeeProjects: any[], targetMonth?: Date): EmployeeCalendarData {
  235. const currentMonth = targetMonth || new Date();
  236. const year = currentMonth.getFullYear();
  237. const month = currentMonth.getMonth();
  238. // 获取当月天数
  239. const daysInMonth = new Date(year, month + 1, 0).getDate();
  240. const days: EmployeeCalendarDay[] = [];
  241. const today = new Date();
  242. today.setHours(0, 0, 0, 0);
  243. // 生成当月每一天的数据
  244. for (let day = 1; day <= daysInMonth; day++) {
  245. const date = new Date(year, month, day);
  246. const dateStr = date.toISOString().split('T')[0];
  247. // 找出该日期相关的项目(项目进行中且在当天范围内)
  248. const dayProjects = employeeProjects.filter(p => {
  249. // 移除对已完成项目的过滤,以便在日历中显示历史负载
  250. // if (p.status === '已完成' || p.status === '已交付') {
  251. // return false;
  252. // }
  253. const projectData = p.data || {};
  254. // 2. 获取真实的项目开始时间 (逻辑与 DesignerWorkloadService 保持一致)
  255. const realStartDate = normalizeDateInput(
  256. projectData.phaseDeadlines?.modeling?.startDate ||
  257. projectData.requirementsConfirmedAt ||
  258. p.createdAt,
  259. new Date()
  260. );
  261. // 3. 获取真实的交付日期 (逻辑与 DesignerWorkloadService 保持一致)
  262. let proposedEndDate = p.deadline || projectData.phaseDeadlines?.postProcessing?.deadline;
  263. let realEndDate: Date;
  264. if (proposedEndDate) {
  265. const proposed = normalizeDateInput(proposedEndDate, realStartDate);
  266. // fix: 只要有明确的deadline,就使用它
  267. realEndDate = proposed;
  268. } else {
  269. realEndDate = addDays(realStartDate, 30);
  270. }
  271. // 归一化为当天0点,便于比较
  272. const rangeStart = new Date(realStartDate);
  273. rangeStart.setHours(0, 0, 0, 0);
  274. const rangeEnd = new Date(realEndDate);
  275. rangeEnd.setHours(0, 0, 0, 0);
  276. const inRange = date >= rangeStart && date <= rangeEnd;
  277. return inRange;
  278. }).map(p => {
  279. const getDate = (dateValue: any) => {
  280. if (!dateValue) return undefined;
  281. if (dateValue.toDate && typeof dateValue.toDate === 'function') {
  282. return dateValue.toDate();
  283. }
  284. if (dateValue instanceof Date) {
  285. return dateValue;
  286. }
  287. return new Date(dateValue);
  288. };
  289. return {
  290. id: p.id,
  291. name: p.name,
  292. deadline: getDate(p.deadline)
  293. };
  294. });
  295. days.push({
  296. date,
  297. projectCount: dayProjects.length,
  298. projects: dayProjects,
  299. isToday: date.getTime() === today.getTime(),
  300. isCurrentMonth: true
  301. });
  302. }
  303. // 补齐前后的日期(保证从周日开始)
  304. const firstDay = new Date(year, month, 1);
  305. const firstDayOfWeek = firstDay.getDay(); // 0=周日
  306. // 前置补齐(上个月的日期)
  307. for (let i = firstDayOfWeek - 1; i >= 0; i--) {
  308. const date = new Date(year, month, -i);
  309. days.unshift({
  310. date,
  311. projectCount: 0,
  312. projects: [],
  313. isToday: false,
  314. isCurrentMonth: false
  315. });
  316. }
  317. // 后置补齐(下个月的日期,保证总数是7的倍数)
  318. const remainder = days.length % 7;
  319. if (remainder !== 0) {
  320. const needed = 7 - remainder;
  321. for (let i = 1; i <= needed; i++) {
  322. const date = new Date(year, month + 1, i);
  323. days.push({
  324. date,
  325. projectCount: 0,
  326. projects: [],
  327. isToday: false,
  328. isCurrentMonth: false
  329. });
  330. }
  331. }
  332. return {
  333. currentMonth: new Date(year, month, 1),
  334. days
  335. };
  336. }
  337. // 生成红色标记说明
  338. private generateRedMarkExplanation(employeeName: string, leaveRecords: LeaveRecord[], projectCount: number): string {
  339. const explanations: string[] = [];
  340. // 检查请假情况
  341. const leaveDays = leaveRecords.filter(record => record.isLeave);
  342. if (leaveDays.length > 0) {
  343. leaveDays.forEach(leave => {
  344. const date = new Date(leave.date);
  345. const dateStr = `${date.getMonth() + 1}月${date.getDate()}日`;
  346. explanations.push(`${dateStr}(${leave.reason || '请假'})`);
  347. });
  348. }
  349. // 检查项目繁忙情况
  350. if (projectCount >= 3) {
  351. const today = new Date();
  352. const dateStr = `${today.getMonth() + 1}月${today.getDate()}日`;
  353. explanations.push(`${dateStr}(${projectCount}个项目繁忙)`);
  354. }
  355. if (explanations.length === 0) {
  356. return '当前无红色标记时段';
  357. }
  358. return `甘特图中红色时段说明:${explanations.map((exp, index) => `${index + 1}${exp}`).join(';')}`;
  359. }
  360. /**
  361. * 关闭面板
  362. */
  363. onClose(): void {
  364. this.close.emit();
  365. this.showFullSurvey = false;
  366. this.closeCalendarProjectList();
  367. }
  368. /**
  369. * 切换月份
  370. */
  371. onChangeMonth(direction: number): void {
  372. // 如果是外部数据模式,发出事件让父组件处理
  373. if (this.employeeDetail) {
  374. this.calendarMonthChange.emit(direction);
  375. return;
  376. }
  377. // 内部模式处理
  378. if (!this.internalEmployeeDetail?.calendarData) {
  379. return;
  380. }
  381. const currentMonth = this.internalEmployeeDetail.calendarData.currentMonth;
  382. const newMonth = new Date(currentMonth);
  383. newMonth.setMonth(newMonth.getMonth() + direction);
  384. // 重新生成日历数据
  385. const newCalendarData = this.generateEmployeeCalendar(
  386. this.employeeName,
  387. this.projects,
  388. newMonth
  389. );
  390. // 更新员工详情中的日历数据
  391. this.internalEmployeeDetail = {
  392. ...this.internalEmployeeDetail,
  393. calendarData: newCalendarData
  394. };
  395. }
  396. /**
  397. * 日历日期点击
  398. */
  399. onCalendarDayClick(day: EmployeeCalendarDay): void {
  400. // 发出事件
  401. this.calendarDayClick.emit(day);
  402. // 内部处理
  403. if (!day.isCurrentMonth || day.projectCount === 0) {
  404. return;
  405. }
  406. this.selectedDate = day.date;
  407. this.selectedDayProjects = day.projects;
  408. this.showCalendarProjectList = true;
  409. }
  410. /**
  411. * 关闭项目列表弹窗
  412. */
  413. closeCalendarProjectList(): void {
  414. this.showCalendarProjectList = false;
  415. this.selectedDate = null;
  416. this.selectedDayProjects = [];
  417. }
  418. /**
  419. * 项目点击
  420. */
  421. onProjectClick(projectId: string): void {
  422. this.projectClick.emit(projectId);
  423. this.closeCalendarProjectList();
  424. }
  425. /**
  426. * 打开“设计师详细日历”弹窗(复用订单分配页的日历)
  427. * 将当前员工信息适配为 DesignerCalendar 组件的数据结构
  428. */
  429. openDesignerCalendar(): void {
  430. const detail = this.currentEmployeeDetail;
  431. if (!detail) return;
  432. const name = detail.name || '设计师';
  433. const currentProjects = detail.currentProjects || 0;
  434. // 将已有的 employeeDetail.calendarData 映射为日历事件(粗粒度:有项目视为当日有工作)
  435. const upcomingEvents: CalendarDesigner['upcomingEvents'] = [];
  436. const days = detail.calendarData?.days || [];
  437. for (const day of days) {
  438. if (day.projectCount > 0) {
  439. upcomingEvents.push({
  440. id: `${day.date.getTime()}`,
  441. date: day.date,
  442. title: `${day.projectCount}个项目`,
  443. type: 'project',
  444. duration: 6
  445. });
  446. }
  447. }
  448. // 适配为日历组件的设计师数据(单人视图)
  449. this.calendarDesigners = [{
  450. id: detail.profileId || name,
  451. name,
  452. groupId: '',
  453. groupName: '',
  454. isLeader: false,
  455. status: currentProjects >= 3 ? 'busy' : 'available',
  456. currentProjects,
  457. upcomingEvents,
  458. workload: Math.min(100, currentProjects * 30)
  459. }];
  460. this.showDesignerCalendar = true;
  461. }
  462. closeDesignerCalendar(): void {
  463. this.showDesignerCalendar = false;
  464. this.calendarDesigners = [];
  465. }
  466. /**
  467. * 刷新问卷
  468. */
  469. async onRefreshSurvey(): Promise<void> {
  470. // 如果是外部数据模式,发出事件让父组件处理
  471. if (this.employeeDetail) {
  472. this.refreshSurvey.emit();
  473. return;
  474. }
  475. if (this.refreshingSurvey || !this.internalEmployeeDetail) {
  476. return;
  477. }
  478. try {
  479. this.refreshingSurvey = true;
  480. console.log('🔄 刷新问卷状态...');
  481. await this.loadSurveyData(this.employeeName);
  482. console.log('✅ 问卷状态刷新成功');
  483. } catch (error) {
  484. console.error('❌ 刷新问卷状态失败:', error);
  485. } finally {
  486. this.refreshingSurvey = false;
  487. }
  488. }
  489. /**
  490. * 切换问卷显示模式
  491. */
  492. toggleSurveyDisplay(): void {
  493. this.showFullSurvey = !this.showFullSurvey;
  494. }
  495. /**
  496. * 获取能力画像摘要
  497. */
  498. getCapabilitySummary(answers: any[]): any {
  499. const findAnswer = (questionId: string) => {
  500. const item = answers.find((a: any) => a.questionId === questionId);
  501. return item?.answer;
  502. };
  503. const formatArray = (value: any): string => {
  504. if (Array.isArray(value)) {
  505. return value.join('、');
  506. }
  507. return value || '未填写';
  508. };
  509. return {
  510. styles: formatArray(findAnswer('q1_expertise_styles')),
  511. spaces: formatArray(findAnswer('q2_expertise_spaces')),
  512. advantages: formatArray(findAnswer('q3_technical_advantages')),
  513. difficulty: findAnswer('q5_project_difficulty') || '未填写',
  514. capacity: findAnswer('q7_weekly_capacity') || '未填写',
  515. urgent: findAnswer('q8_urgent_willingness') || '未填写',
  516. urgentLimit: findAnswer('q8_urgent_limit') || '',
  517. feedback: findAnswer('q9_progress_feedback') || '未填写',
  518. communication: formatArray(findAnswer('q12_communication_methods'))
  519. };
  520. }
  521. /**
  522. * 获取请假类型显示文本
  523. */
  524. getLeaveTypeText(leaveType?: string): string {
  525. const typeMap: Record<string, string> = {
  526. 'sick': '病假',
  527. 'personal': '事假',
  528. 'annual': '年假',
  529. 'other': '其他'
  530. };
  531. return typeMap[leaveType || ''] || '未知';
  532. }
  533. /**
  534. * 查看项目进度
  535. */
  536. async onViewProgress(projectId: string, event?: Event): Promise<void> {
  537. if (event) {
  538. event.stopPropagation();
  539. }
  540. this.selectedProjectForProgress = projectId;
  541. this.loadingProgress = true;
  542. this.showProgressModal = true;
  543. try {
  544. console.log('📊 加载项目进度数据:', projectId);
  545. this.progressSummary = await this.projectSpaceDeliverableService.getProjectSpaceDeliverableSummary(projectId);
  546. console.log('✅ 项目进度数据加载成功:', this.progressSummary);
  547. } catch (error) {
  548. console.error('❌ 加载项目进度数据失败:', error);
  549. this.progressSummary = null;
  550. } finally {
  551. this.loadingProgress = false;
  552. this.cdr.markForCheck();
  553. }
  554. }
  555. /**
  556. * 关闭进度弹窗
  557. */
  558. closeProgressModal(): void {
  559. this.showProgressModal = false;
  560. this.selectedProjectForProgress = '';
  561. this.progressSummary = null;
  562. }
  563. /**
  564. * 阻止事件冒泡
  565. */
  566. stopPropagation(event: Event): void {
  567. event.stopPropagation();
  568. }
  569. }