dashboard.ts 100 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481
  1. import { Component, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { RouterModule } from '@angular/router';
  4. import { MatCardModule } from '@angular/material/card';
  5. import { MatButtonModule } from '@angular/material/button';
  6. import { MatTabsModule } from '@angular/material/tabs';
  7. import { MatIconModule } from '@angular/material/icon';
  8. import { MatCheckboxModule } from '@angular/material/checkbox';
  9. import { MatChipsModule } from '@angular/material/chips';
  10. import { MatProgressBarModule } from '@angular/material/progress-bar';
  11. import { MatBadgeModule } from '@angular/material/badge';
  12. import { MatFormFieldModule } from '@angular/material/form-field';
  13. import { MatSelectModule } from '@angular/material/select';
  14. import { MatInputModule } from '@angular/material/input';
  15. import { MatTableModule } from '@angular/material/table';
  16. import { MatButtonToggleModule } from '@angular/material/button-toggle';
  17. import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
  18. import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
  19. import { MatDialogModule, MatDialog } from '@angular/material/dialog';
  20. import { DragDropModule, CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
  21. import { Chart, ChartConfiguration, registerables } from 'chart.js';
  22. import { trigger, state, style, transition, animate } from '@angular/animations';
  23. import { DoubaoAiService, ResumeAnalysisRequest, ResumeAnalysisResponse } from '../../../services/doubao-ai.service';
  24. import { ResignationDetailPanelComponent, DetailAnalysis, ImprovementPlan } from './resignation-detail-panel.component';
  25. import { AddComparisonDialogComponent, ComparisonItemData } from './add-comparison-dialog.component';
  26. Chart.register(...registerables);
  27. // 数据模型定义
  28. export interface TodoItem {
  29. id: number;
  30. title: string;
  31. description: string;
  32. priority: 'high' | 'medium' | 'low';
  33. status: 'pending' | 'completed' | 'in_progress';
  34. type: 'resume' | 'onboarding' | 'resignation';
  35. }
  36. export interface RankDistribution {
  37. junior: number;
  38. intermediate: number;
  39. senior: number;
  40. }
  41. export interface MonthlyData {
  42. month: string;
  43. hired: number;
  44. left: number;
  45. notes?: string;
  46. }
  47. export interface DepartmentPerformance {
  48. department: string;
  49. completionRate: number;
  50. excellentWorkRate: number;
  51. satisfactionRate: number;
  52. overdueRate: number;
  53. }
  54. export interface OnboardingCheckpoint {
  55. id: number;
  56. title: string;
  57. description: string;
  58. dueDate: Date;
  59. completed: boolean;
  60. interviewTemplate: string[];
  61. }
  62. // AI简历分析相关接口
  63. export interface MatchDimension {
  64. id: number;
  65. name: string;
  66. score: number;
  67. level: 'high' | 'medium' | 'low';
  68. icon: string;
  69. }
  70. export interface Recommendation {
  71. title: string;
  72. level: 'recommend' | 'consider' | 'reject';
  73. levelText: string;
  74. icon: string;
  75. summary: string;
  76. reasons: string[];
  77. concerns: string[];
  78. }
  79. export interface ScreeningInfo {
  80. id: number;
  81. title: string;
  82. detail: string;
  83. status: 'pass' | 'warning' | 'fail';
  84. statusText: string;
  85. icon: string;
  86. }
  87. export interface RecruitmentStage {
  88. id: string;
  89. title: string;
  90. status: 'completed' | 'active' | 'pending' | 'blocked';
  91. statusText: string;
  92. icon: string;
  93. candidateCount: number;
  94. passRate: number;
  95. evaluator?: string;
  96. lastUpdate: Date;
  97. nextAction?: string;
  98. evaluationResults?: EvaluationResult[];
  99. }
  100. export interface EvaluationResult {
  101. candidateName: string;
  102. result: 'pass' | 'fail' | 'pending';
  103. evaluator: string;
  104. evaluationTime: Date;
  105. score?: number;
  106. comments?: string;
  107. }
  108. export interface PerformanceMetric {
  109. id: string;
  110. title: string;
  111. value: string;
  112. unit: string;
  113. target: string;
  114. achievement: string;
  115. achievementClass: string;
  116. period: string;
  117. icon: string;
  118. iconClass: string;
  119. status: 'excellent' | 'good' | 'warning' | 'poor';
  120. trend: {
  121. type: 'positive' | 'negative' | 'neutral';
  122. value: string;
  123. label: string;
  124. icon: string;
  125. };
  126. progressValue: number;
  127. progressClass: string;
  128. }
  129. @Component({
  130. selector: 'app-hr-dashboard',
  131. standalone: true,
  132. imports: [
  133. CommonModule,
  134. RouterModule,
  135. MatCardModule,
  136. MatButtonModule,
  137. MatTabsModule,
  138. MatIconModule,
  139. MatCheckboxModule,
  140. MatChipsModule,
  141. MatProgressBarModule,
  142. MatBadgeModule,
  143. MatFormFieldModule,
  144. MatSelectModule,
  145. MatInputModule,
  146. DragDropModule,
  147. MatTableModule,
  148. MatButtonToggleModule,
  149. MatProgressSpinnerModule,
  150. MatSnackBarModule,
  151. MatDialogModule,
  152. ResignationDetailPanelComponent
  153. ],
  154. templateUrl: './dashboard.html',
  155. styleUrls: ['./dashboard.scss'],
  156. animations: [
  157. trigger('slideInOut', [
  158. transition(':enter', [
  159. style({ height: '0', opacity: 0, overflow: 'hidden' }),
  160. animate('300ms ease-in-out', style({ height: '*', opacity: 1 }))
  161. ]),
  162. transition(':leave', [
  163. animate('300ms ease-in-out', style({ height: '0', opacity: 0, overflow: 'hidden' }))
  164. ])
  165. ])
  166. ]
  167. })
  168. export class Dashboard implements OnInit, AfterViewInit {
  169. @ViewChild('pieChart', { static: false }) pieChartRef!: ElementRef<HTMLCanvasElement>;
  170. @ViewChild('lineChart', { static: false }) lineChartRef!: ElementRef<HTMLCanvasElement>;
  171. @ViewChild('radarChart', { static: false }) radarChartRef!: ElementRef<HTMLCanvasElement>;
  172. @ViewChild('resignationChart', { static: false }) resignationChartRef!: ElementRef<HTMLCanvasElement>;
  173. @ViewChild('comparisonChart', { static: false }) comparisonChartRef!: ElementRef<HTMLCanvasElement>;
  174. constructor(
  175. private doubaoAiService: DoubaoAiService,
  176. private snackBar: MatSnackBar,
  177. private dialog: MatDialog
  178. ) {
  179. Chart.register(...registerables);
  180. }
  181. private pieChart!: Chart;
  182. private lineChart!: Chart;
  183. private radarChart!: Chart;
  184. private resignationChart!: Chart;
  185. private comparisonChart!: Chart;
  186. // 当前激活的标签页
  187. activeTab: 'visualization' | 'recruitment' | 'performance' | 'onboarding' = 'visualization';
  188. // 对比图表类型
  189. chartType: 'bar' | 'line' | 'radar' = 'line';
  190. // 待办事项是否展开
  191. isTodoExpanded = false;
  192. // 当前页面
  193. currentPage: string = 'dashboard';
  194. // 待办事项展开状态
  195. showTodoList: boolean = false;
  196. // 待办事项列表(用于右侧展示)
  197. todoList = [
  198. {
  199. id: 1,
  200. title: '简历初筛',
  201. description: '筛选新收到的设计师简历',
  202. priority: 'high',
  203. dueDate: '2024-01-25'
  204. },
  205. {
  206. id: 2,
  207. title: '入职评定',
  208. description: '完成新员工入职评定表',
  209. priority: 'medium',
  210. dueDate: '2024-01-28'
  211. },
  212. {
  213. id: 4,
  214. title: '离职面谈',
  215. description: '安排资深设计师离职面谈',
  216. priority: 'medium',
  217. dueDate: '2024-02-02'
  218. }
  219. ];
  220. // 模拟数据
  221. todoItems: TodoItem[] = [
  222. {
  223. id: 1,
  224. title: '简历初筛',
  225. description: '筛选新收到的设计师简历',
  226. priority: 'high',
  227. status: 'pending',
  228. type: 'resume'
  229. },
  230. {
  231. id: 2,
  232. title: '入职评定',
  233. description: '完成新员工入职评定表',
  234. priority: 'medium',
  235. status: 'pending',
  236. type: 'onboarding'
  237. },
  238. {
  239. id: 4,
  240. title: '离职面谈',
  241. description: '安排资深设计师离职面谈',
  242. priority: 'medium',
  243. status: 'pending',
  244. type: 'resignation'
  245. }
  246. ];
  247. // 职级分布数据
  248. rankDistribution = [
  249. { level: '初级设计师', count: 15, percentage: 45, color: '#4CAF50' },
  250. { level: '中级设计师', count: 12, percentage: 36, color: '#FF9800' },
  251. { level: '高级设计师', count: 6, percentage: 19, color: '#2196F3' }
  252. ];
  253. monthlyHireData: MonthlyData[] = [
  254. { month: '10月', hired: 8, left: 3, notes: '秋季招聘高峰' },
  255. { month: '11月', hired: 5, left: 2, notes: '' },
  256. { month: '12月', hired: 3, left: 4, notes: '年底离职高峰' },
  257. { month: '1月', hired: 10, left: 1, notes: '新年新气象' },
  258. { month: '2月', hired: 6, left: 2, notes: '' },
  259. { month: '3月', hired: 12, left: 3, notes: '春季招聘启动' }
  260. ];
  261. // 关键节点数据
  262. keyNotes = [
  263. { month: '3月', description: '入职10人:因春季招聘' },
  264. { month: '5月', description: '离职5人:因项目不饱和' },
  265. { month: '8月', description: '入职15人:暑期实习转正' },
  266. { month: '12月', description: '离职8人:年底跳槽高峰' }
  267. ];
  268. // 关键岗位空缺数据
  269. keyVacancies = [
  270. { position: '高级UI设计师', count: 2, priority: 'high', duration: 45 },
  271. { position: '3D建模师', count: 1, priority: 'medium', duration: 20 },
  272. { position: '前端开发工程师', count: 1, priority: 'medium', duration: 15 },
  273. { position: '产品经理', count: 1, priority: 'high', duration: 60 }
  274. ];
  275. // 新人列表数据
  276. newbieList = [
  277. { id: 1, name: '张三', position: 'UI设计师', joinDate: '2024-01-15', progress: 75 },
  278. { id: 2, name: '李四', position: '前端开发', joinDate: '2024-01-20', progress: 60 },
  279. { id: 3, name: '王五', position: '3D建模师', joinDate: '2024-02-01', progress: 40 }
  280. ];
  281. departmentPerformance: DepartmentPerformance[] = [
  282. {
  283. department: 'UI设计部',
  284. completionRate: 92,
  285. excellentWorkRate: 78,
  286. satisfactionRate: 88,
  287. overdueRate: 8
  288. },
  289. {
  290. department: '3D建模部',
  291. completionRate: 85,
  292. excellentWorkRate: 82,
  293. satisfactionRate: 90,
  294. overdueRate: 15
  295. },
  296. {
  297. department: '前端开发部',
  298. completionRate: 88,
  299. excellentWorkRate: 75,
  300. satisfactionRate: 85,
  301. overdueRate: 12
  302. }
  303. ];
  304. onboardingCheckpoints: OnboardingCheckpoint[] = [
  305. {
  306. id: 1,
  307. title: '入职1周面谈',
  308. description: '了解新人工作适应情况',
  309. dueDate: new Date(),
  310. completed: false,
  311. interviewTemplate: [
  312. '工作环境是否适应?',
  313. '是否有不清楚的工作流程?',
  314. '团队沟通是否顺畅?',
  315. '是否需要额外的工作工具?'
  316. ]
  317. },
  318. {
  319. id: 2,
  320. title: '入职1个月评估',
  321. description: '评估新人技能掌握情况',
  322. dueDate: new Date(Date.now() + 21 * 24 * 60 * 60 * 1000),
  323. completed: false,
  324. interviewTemplate: [
  325. '主要工作技能掌握程度?',
  326. '遇到的最大挑战是什么?',
  327. '对团队文化的感受?',
  328. '个人职业发展期望?'
  329. ]
  330. },
  331. {
  332. id: 3,
  333. title: '入职3个月总结',
  334. description: '全面评估新人表现',
  335. dueDate: new Date(Date.now() + 83 * 24 * 60 * 60 * 1000),
  336. completed: false,
  337. interviewTemplate: [
  338. '工作成果总体评价',
  339. '需要改进的技能领域',
  340. '长期职业规划讨论',
  341. '转正评估准备'
  342. ]
  343. }
  344. ];
  345. ngOnInit() {
  346. // 注册Chart.js组件
  347. Chart.register(...registerables);
  348. // 初始化图表数据
  349. this.initCharts();
  350. }
  351. ngAfterViewInit() {
  352. // 延迟初始化图表,确保DOM已渲染
  353. setTimeout(() => {
  354. this.initializeCharts();
  355. this.initScrollIndicator();
  356. }, 100);
  357. }
  358. ngOnDestroy() {
  359. // 销毁所有图表实例,防止内存泄漏
  360. if (this.pieChart) {
  361. this.pieChart.destroy();
  362. }
  363. if (this.lineChart) {
  364. this.lineChart.destroy();
  365. }
  366. if (this.radarChart) {
  367. this.radarChart.destroy();
  368. }
  369. if (this.comparisonChart) {
  370. this.comparisonChart.destroy();
  371. }
  372. }
  373. // 切换标签页
  374. switchTab(tab: 'visualization' | 'recruitment' | 'performance' | 'onboarding') {
  375. this.activeTab = tab;
  376. // 如果切换到数据可视化页面,重新初始化图表
  377. if (tab === 'visualization') {
  378. setTimeout(() => this.initializeCharts(), 100);
  379. }
  380. // 如果切换到绩效统计页面,初始化对比图表
  381. if (tab === 'performance') {
  382. setTimeout(() => this.initComparisonChart(), 100);
  383. }
  384. }
  385. // 绩效指标数据
  386. performanceMetrics: PerformanceMetric[] = [
  387. {
  388. id: 'project-completion',
  389. title: '项目完成率',
  390. value: '89',
  391. unit: '%',
  392. target: '85%',
  393. achievement: '104.7%',
  394. achievementClass: 'excellent',
  395. period: '本月',
  396. icon: 'assignment_turned_in',
  397. iconClass: 'success-icon',
  398. status: 'excellent',
  399. trend: {
  400. type: 'positive',
  401. value: '+5.2%',
  402. label: '较上月',
  403. icon: 'trending_up'
  404. },
  405. progressValue: 89,
  406. progressClass: 'success-progress'
  407. },
  408. {
  409. id: 'quality-rate',
  410. title: '优秀作品率',
  411. value: '76',
  412. unit: '%',
  413. target: '80%',
  414. achievement: '95.0%',
  415. achievementClass: 'good',
  416. period: '本月',
  417. icon: 'star',
  418. iconClass: 'warning-icon',
  419. status: 'good',
  420. trend: {
  421. type: 'positive',
  422. value: '+2.8%',
  423. label: '较上月',
  424. icon: 'trending_up'
  425. },
  426. progressValue: 76,
  427. progressClass: 'warning-progress'
  428. },
  429. {
  430. id: 'satisfaction',
  431. title: '客户满意度',
  432. value: '4.6',
  433. unit: '/5.0',
  434. target: '4.5',
  435. achievement: '102.2%',
  436. achievementClass: 'excellent',
  437. period: '本月',
  438. icon: 'sentiment_satisfied',
  439. iconClass: 'success-icon',
  440. status: 'excellent',
  441. trend: {
  442. type: 'positive',
  443. value: '+0.3',
  444. label: '较上月',
  445. icon: 'trending_up'
  446. },
  447. progressValue: 92,
  448. progressClass: 'success-progress'
  449. },
  450. {
  451. id: 'overdue-rate',
  452. title: '项目逾期率',
  453. value: '8',
  454. unit: '%',
  455. target: '≤10%',
  456. achievement: '120.0%',
  457. achievementClass: 'good',
  458. period: '本月',
  459. icon: 'schedule',
  460. iconClass: 'success-icon',
  461. status: 'good',
  462. trend: {
  463. type: 'positive',
  464. value: '-1.5%',
  465. label: '较上月',
  466. icon: 'trending_down'
  467. },
  468. progressValue: 20,
  469. progressClass: 'success-progress'
  470. },
  471. {
  472. id: 'team-efficiency',
  473. title: '团队效率指数',
  474. value: '92',
  475. unit: '分',
  476. target: '90分',
  477. achievement: '102.2%',
  478. achievementClass: 'excellent',
  479. period: '本月',
  480. icon: 'groups',
  481. iconClass: 'success-icon',
  482. status: 'excellent',
  483. trend: {
  484. type: 'positive',
  485. value: '+4.1',
  486. label: '较上月',
  487. icon: 'trending_up'
  488. },
  489. progressValue: 92,
  490. progressClass: 'success-progress'
  491. }
  492. ];
  493. // 筛选相关属性
  494. selectedDepartment: string = '';
  495. selectedLevel: string = '';
  496. selectedTimeRange: string = 'month';
  497. isFilterLoading: boolean = false;
  498. quickFilter: string = '';
  499. // 绩效对比相关属性
  500. comparisonMode: 'horizontal' | 'vertical' = 'horizontal';
  501. selectedComparisonDimension: string = 'department';
  502. selectedComparisonMetric: string[] = ['completion', 'quality'];
  503. comparisonChartType: 'bar' | 'line' | 'radar' = 'bar';
  504. horizontalComparisonData: any[] = [
  505. {
  506. id: 1,
  507. name: '技术部',
  508. icon: 'code',
  509. iconClass: 'tech-icon',
  510. completion: '92%',
  511. quality: '88%',
  512. efficiency: '85%',
  513. satisfaction: '90%',
  514. innovation: '95%'
  515. },
  516. {
  517. id: 2,
  518. name: '设计部',
  519. icon: 'palette',
  520. iconClass: 'design-icon',
  521. completion: '88%',
  522. quality: '92%',
  523. efficiency: '82%',
  524. satisfaction: '87%',
  525. innovation: '90%'
  526. },
  527. {
  528. id: 3,
  529. name: '产品部',
  530. icon: 'lightbulb',
  531. iconClass: 'product-icon',
  532. completion: '85%',
  533. quality: '85%',
  534. efficiency: '88%',
  535. satisfaction: '85%',
  536. innovation: '88%'
  537. },
  538. {
  539. id: 4,
  540. name: '运营部',
  541. icon: 'trending_up',
  542. iconClass: 'operation-icon',
  543. completion: '90%',
  544. quality: '83%',
  545. efficiency: '90%',
  546. satisfaction: '88%',
  547. innovation: '82%'
  548. }
  549. ];
  550. verticalComparisonData: any[] = [
  551. {
  552. id: 1,
  553. name: '技术部',
  554. category: '研发部门',
  555. icon: 'code',
  556. iconClass: 'tech-icon',
  557. completion: '92%',
  558. quality: '88%',
  559. efficiency: '85%',
  560. satisfaction: '90%',
  561. innovation: '95%',
  562. overallScore: 90,
  563. rank: 1
  564. },
  565. {
  566. id: 2,
  567. name: '设计部',
  568. category: '创意部门',
  569. icon: 'palette',
  570. iconClass: 'design-icon',
  571. completion: '88%',
  572. quality: '92%',
  573. efficiency: '82%',
  574. satisfaction: '87%',
  575. innovation: '90%',
  576. overallScore: 88,
  577. rank: 2
  578. },
  579. {
  580. id: 3,
  581. name: '产品部',
  582. category: '策略部门',
  583. icon: 'lightbulb',
  584. iconClass: 'product-icon',
  585. completion: '85%',
  586. quality: '85%',
  587. efficiency: '88%',
  588. satisfaction: '85%',
  589. innovation: '88%',
  590. overallScore: 86,
  591. rank: 3
  592. },
  593. {
  594. id: 4,
  595. name: '运营部',
  596. category: '执行部门',
  597. icon: 'trending_up',
  598. iconClass: 'operation-icon',
  599. completion: '90%',
  600. quality: '83%',
  601. efficiency: '90%',
  602. satisfaction: '88%',
  603. innovation: '82%',
  604. overallScore: 87,
  605. rank: 4
  606. }
  607. ];
  608. horizontalDisplayedColumns: string[] = ['name', 'completion', 'quality', 'actions'];
  609. // 离职原因分析相关属性
  610. resignationTimeRange: string = 'quarter';
  611. reasonsChartType: 'pie' | 'doughnut' | 'bar' = 'pie';
  612. totalResignations: number = 45;
  613. resignationRate: number = 8.5;
  614. averageTenure: number = 18;
  615. resignationCost: number = 125;
  616. resignationDepartments = [
  617. { id: 'tech', name: '技术部', count: 15, selected: true },
  618. { id: 'design', name: '设计部', count: 8, selected: true },
  619. { id: 'product', name: '产品部', count: 12, selected: true },
  620. { id: 'operation', name: '运营部', count: 10, selected: true }
  621. ];
  622. resignationLevels = [
  623. { id: 'junior', name: '初级', count: 18, selected: true },
  624. { id: 'middle', name: '中级', count: 20, selected: true },
  625. { id: 'senior', name: '高级', count: 7, selected: true }
  626. ];
  627. // 详情面板相关
  628. showDetailPanel = false;
  629. selectedReason: any = null;
  630. selectedDetailAnalysis: DetailAnalysis | null = null;
  631. selectedImprovementPlan: ImprovementPlan | null = null;
  632. resignationReasons = [
  633. {
  634. id: 'salary',
  635. name: '薪资待遇',
  636. category: 'compensation',
  637. categoryName: '薪酬福利',
  638. icon: 'payments',
  639. percentage: 28.5,
  640. count: 14,
  641. description: '薪资水平低于市场平均水平,缺乏有竞争力的薪酬体系',
  642. trend: { direction: 'up', value: 3.2 }
  643. },
  644. {
  645. id: 'career',
  646. name: '职业发展',
  647. category: 'development',
  648. categoryName: '发展机会',
  649. icon: 'trending_up',
  650. percentage: 22.3,
  651. count: 10,
  652. description: '缺乏明确的职业发展路径和晋升机会',
  653. trend: { direction: 'down', value: 2.1 }
  654. },
  655. {
  656. id: 'workload',
  657. name: '工作压力',
  658. category: 'worklife',
  659. categoryName: '工作环境',
  660. icon: 'work',
  661. percentage: 18.7,
  662. count: 8,
  663. description: '工作强度过大,工作与生活平衡难以维持',
  664. trend: { direction: 'up', value: 3.5 }
  665. },
  666. {
  667. id: 'management',
  668. name: '管理问题',
  669. category: 'management',
  670. categoryName: '管理层面',
  671. icon: 'supervisor_account',
  672. percentage: 15.2,
  673. count: 7,
  674. description: '管理方式不当,缺乏有效的沟通和反馈机制',
  675. trend: { direction: 'down', value: 1.8 }
  676. },
  677. {
  678. id: 'culture',
  679. name: '企业文化',
  680. category: 'culture',
  681. categoryName: '文化氛围',
  682. icon: 'groups',
  683. percentage: 10.1,
  684. count: 5,
  685. description: '企业文化与个人价值观不匹配,团队氛围不佳',
  686. trend: { direction: 'up', value: 2.3 }
  687. },
  688. {
  689. id: 'personal',
  690. name: '个人原因',
  691. category: 'personal',
  692. categoryName: '个人因素',
  693. icon: 'person',
  694. percentage: 5.2,
  695. count: 2,
  696. description: '个人家庭、健康等因素导致的离职',
  697. trend: { direction: 'down', value: 0.8 }
  698. }
  699. ];
  700. // 简历分析相关属性
  701. isDragOver: boolean = false;
  702. showAnalysisResults: boolean = false;
  703. isAnalyzing: boolean = false;
  704. currentAnalysisFile: File | null = null;
  705. analysisProgress: number = 0;
  706. matchDimensions: MatchDimension[] = [
  707. { id: 1, name: '建模经验', score: 92, level: 'high', icon: 'view_in_ar' },
  708. { id: 2, name: 'UI设计', score: 85, level: 'high', icon: 'design_services' },
  709. { id: 3, name: '用户体验', score: 78, level: 'medium', icon: 'psychology' },
  710. { id: 4, name: '团队协作', score: 88, level: 'high', icon: 'groups' },
  711. { id: 5, name: '项目管理', score: 65, level: 'medium', icon: 'task_alt' }
  712. ];
  713. recommendation: Recommendation = {
  714. title: '强烈推荐进入面试环节',
  715. level: 'recommend',
  716. levelText: '推荐',
  717. icon: 'thumb_up',
  718. summary: '候选人在核心技能方面表现优秀,具备丰富的建模经验和良好的设计基础,建议安排技术面试。',
  719. reasons: [
  720. '3年以上3D建模经验,熟练掌握Maya、Blender等主流软件',
  721. 'UI设计基础扎实,有完整的项目作品集',
  722. '具备良好的团队协作能力和沟通技巧',
  723. '学习能力强,能够快速适应新技术和工具'
  724. ],
  725. concerns: [
  726. '项目管理经验相对较少,需要在实际工作中加强',
  727. '对公司业务领域了解有限,需要一定的适应期'
  728. ]
  729. };
  730. screeningInfo: ScreeningInfo[] = [
  731. { id: 1, title: '学历要求', detail: '本科及以上学历', status: 'pass', statusText: '符合', icon: 'school' },
  732. { id: 2, title: '工作经验', detail: '3年相关工作经验', status: 'pass', statusText: '符合', icon: 'work' },
  733. { id: 3, title: '技能匹配', detail: '核心技能匹配度85%', status: 'pass', statusText: '优秀', icon: 'star' },
  734. { id: 4, title: '薪资期望', detail: '期望薪资15K-18K', status: 'warning', statusText: '偏高', icon: 'payments' },
  735. { id: 5, title: '到岗时间', detail: '可立即到岗', status: 'pass', statusText: '符合', icon: 'schedule' }
  736. ];
  737. // 招聘阶段数据
  738. recruitmentStages: RecruitmentStage[] = [
  739. {
  740. id: 'resume-screening',
  741. title: '简历初筛',
  742. status: 'completed',
  743. statusText: '已完成',
  744. icon: 'description',
  745. candidateCount: 45,
  746. passRate: 65,
  747. evaluator: '张经理',
  748. lastUpdate: new Date('2024-01-15T10:30:00'),
  749. nextAction: '进入面试环节',
  750. evaluationResults: [
  751. {
  752. candidateName: '李小明',
  753. result: 'pass',
  754. evaluator: '张经理',
  755. evaluationTime: new Date('2024-01-15T09:00:00'),
  756. score: 85,
  757. comments: '技能匹配度高,经验丰富'
  758. }
  759. ]
  760. },
  761. {
  762. id: 'interview-assessment',
  763. title: '面试评估',
  764. status: 'active',
  765. statusText: '进行中',
  766. icon: 'record_voice_over',
  767. candidateCount: 29,
  768. passRate: 72,
  769. evaluator: '王总监',
  770. lastUpdate: new Date('2024-01-16T14:20:00'),
  771. nextAction: '安排技术面试',
  772. evaluationResults: []
  773. },
  774. {
  775. id: 'onboarding-evaluation',
  776. title: '入职评定',
  777. status: 'pending',
  778. statusText: '待开始',
  779. icon: 'how_to_reg',
  780. candidateCount: 21,
  781. passRate: 90,
  782. evaluator: '人事部',
  783. lastUpdate: new Date('2024-01-14T16:45:00'),
  784. nextAction: '准备入职材料'
  785. },
  786. {
  787. id: 'probation-tracking',
  788. title: '试用期跟踪',
  789. status: 'active',
  790. statusText: '跟踪中',
  791. icon: 'trending_up',
  792. candidateCount: 18,
  793. passRate: 88,
  794. evaluator: '直属主管',
  795. lastUpdate: new Date('2024-01-16T11:15:00'),
  796. nextAction: '月度评估'
  797. }
  798. ];
  799. // 打开晋升规则弹窗
  800. openPromotionRules(): void {
  801. // 这里可以打开一个对话框显示晋升规则
  802. console.log('打开晋升规则弹窗');
  803. // 临时实现:显示alert
  804. alert('晋升规则:\n\n初级→中级:连续3个月优秀作品率超过80%,逾期率低于5%,客户满意度4.5以上\n\n中级→高级:连续6个月优秀作品率超过85%,逾期率低于3%,客户满意度4.8以上,有mentorship经验');
  805. }
  806. // 招聘相关方法
  807. uploadResume() {
  808. // 模拟文件上传过程
  809. const input = document.createElement('input');
  810. input.type = 'file';
  811. input.accept = '.pdf,.doc,.docx';
  812. input.onchange = (event: any) => {
  813. const file = event.target.files[0];
  814. if (file) {
  815. this.handleFileUpload(file);
  816. }
  817. };
  818. input.click();
  819. }
  820. // 拖放相关方法
  821. onDragOver(event: DragEvent) {
  822. event.preventDefault();
  823. event.stopPropagation();
  824. this.isDragOver = true;
  825. }
  826. onDragLeave(event: DragEvent) {
  827. event.preventDefault();
  828. event.stopPropagation();
  829. this.isDragOver = false;
  830. }
  831. onFileDrop(event: DragEvent) {
  832. event.preventDefault();
  833. event.stopPropagation();
  834. this.isDragOver = false;
  835. const files = event.dataTransfer?.files;
  836. if (files && files.length > 0) {
  837. const file = files[0];
  838. this.handleFileUpload(file);
  839. }
  840. }
  841. private async handleFileUpload(file: File) {
  842. // 验证文件类型
  843. const allowedTypes = [
  844. 'application/pdf',
  845. 'application/msword',
  846. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  847. 'text/plain'
  848. ];
  849. if (!allowedTypes.includes(file.type)) {
  850. this.showUploadError('不支持的文件格式,请上传PDF、DOC、DOCX或TXT文件');
  851. return;
  852. }
  853. // 验证文件大小 (10MB)
  854. if (file.size > 10 * 1024 * 1024) {
  855. this.showUploadError('文件大小超过10MB限制');
  856. return;
  857. }
  858. // 开始分析流程
  859. this.currentAnalysisFile = file;
  860. this.isAnalyzing = true;
  861. this.analysisProgress = 0;
  862. this.showAnalysisResults = false;
  863. try {
  864. // 显示开始分析的反馈
  865. this.snackBar.open(`正在分析简历 "${file.name}"...`, '关闭', {
  866. duration: 3000,
  867. horizontalPosition: 'center',
  868. verticalPosition: 'top'
  869. });
  870. // 模拟进度更新
  871. this.updateAnalysisProgress(0);
  872. // 提取文件文本内容
  873. const resumeText = await this.doubaoAiService.extractTextFromFile(file);
  874. // 构建分析请求
  875. const analysisRequest: ResumeAnalysisRequest = {
  876. resumeText: resumeText,
  877. jobPosition: '前端开发工程师', // 可以从当前招聘岗位获取
  878. jobRequirements: [
  879. '3年以上前端开发经验',
  880. '熟练掌握JavaScript、TypeScript',
  881. '熟悉Angular、React或Vue.js框架',
  882. '具备良好的团队协作能力',
  883. '本科及以上学历'
  884. ]
  885. };
  886. // 调用豆包AI进行分析
  887. const analysisResult = await this.doubaoAiService.analyzeResume(analysisRequest).toPromise();
  888. if (analysisResult) {
  889. // 更新分析结果
  890. this.updateAnalysisResults(analysisResult);
  891. this.showAnalysisResults = true;
  892. this.snackBar.open('简历分析完成!', '查看结果', {
  893. duration: 5000,
  894. horizontalPosition: 'center',
  895. verticalPosition: 'top'
  896. });
  897. }
  898. } catch (error) {
  899. console.error('简历分析失败:', error);
  900. this.showUploadError('简历分析失败,请稍后重试');
  901. } finally {
  902. this.isAnalyzing = false;
  903. this.analysisProgress = 100;
  904. }
  905. }
  906. // 招聘阶段相关方法
  907. refreshRecruitmentData(): void {
  908. // 显示加载状态
  909. this.snackBar.open('正在刷新招聘数据...', '', {
  910. duration: 1000,
  911. horizontalPosition: 'center',
  912. verticalPosition: 'top'
  913. });
  914. // 模拟API调用刷新数据
  915. setTimeout(() => {
  916. // 更新招聘阶段数据
  917. this.recruitmentStages.forEach(stage => {
  918. stage.lastUpdate = new Date();
  919. // 随机更新一些数据以模拟真实变化
  920. if (Math.random() > 0.5) {
  921. stage.candidateCount += Math.floor(Math.random() * 3);
  922. stage.passRate = Math.min(100, stage.passRate + Math.floor(Math.random() * 5));
  923. }
  924. });
  925. this.snackBar.open('招聘数据已更新', '关闭', {
  926. duration: 3000,
  927. horizontalPosition: 'center',
  928. verticalPosition: 'top',
  929. panelClass: ['success-snackbar']
  930. });
  931. }, 1000);
  932. }
  933. openStageDetails(stage: RecruitmentStage): void {
  934. // 显示阶段详情信息
  935. const stageInfo = this.getStageDetailInfo(stage);
  936. this.snackBar.open(`${stage.title} - ${stageInfo}`, '查看详情', {
  937. duration: 5000,
  938. horizontalPosition: 'center',
  939. verticalPosition: 'top',
  940. panelClass: ['info-snackbar']
  941. }).onAction().subscribe(() => {
  942. // 这里可以打开详情弹窗或导航到详情页面
  943. console.log('导航到详情页面:', stage);
  944. this.showStageDetailDialog(stage);
  945. });
  946. }
  947. private getStageDetailInfo(stage: RecruitmentStage): string {
  948. switch (stage.status) {
  949. case 'completed':
  950. return `已完成,通过率${stage.passRate}%`;
  951. case 'active':
  952. return `进行中,当前${stage.candidateCount}人`;
  953. case 'pending':
  954. return `待开始,预计${stage.candidateCount}人`;
  955. case 'blocked':
  956. return `已暂停,需要处理`;
  957. default:
  958. return '状态未知';
  959. }
  960. }
  961. private showStageDetailDialog(stage: RecruitmentStage): void {
  962. // 显示详细的阶段信息弹窗
  963. const detailMessage = `
  964. 阶段:${stage.title}
  965. 状态:${stage.statusText}
  966. 候选人数量:${stage.candidateCount}人
  967. 通过率:${stage.passRate}%
  968. 评估人:${stage.evaluator || '待分配'}
  969. 最近更新:${stage.lastUpdate.toLocaleString()}
  970. 下一步行动:${stage.nextAction || '无'}
  971. `;
  972. this.snackBar.open(detailMessage, '关闭', {
  973. duration: 8000,
  974. horizontalPosition: 'center',
  975. verticalPosition: 'top',
  976. panelClass: ['detail-snackbar']
  977. });
  978. }
  979. navigateToOnboarding(): void {
  980. this.snackBar.open('正在跳转到新人跟进模块...', '', {
  981. duration: 2000,
  982. horizontalPosition: 'center',
  983. verticalPosition: 'top'
  984. });
  985. // 切换到入职跟进标签页
  986. setTimeout(() => {
  987. this.switchTab('onboarding');
  988. this.snackBar.open('已切换到新人跟进模块', '关闭', {
  989. duration: 3000,
  990. horizontalPosition: 'center',
  991. verticalPosition: 'top',
  992. panelClass: ['success-snackbar']
  993. });
  994. }, 1000);
  995. }
  996. viewProbationReports(): void {
  997. // 显示试用期报告信息
  998. const reportSummary = `
  999. 试用期跟踪报告:
  1000. - 当前试用期员工:${this.recruitmentStages.find(s => s.id === 'probation-tracking')?.candidateCount || 0}人
  1001. - 通过率:${this.recruitmentStages.find(s => s.id === 'probation-tracking')?.passRate || 0}%
  1002. - 本月评估:3人待评估
  1003. - 转正推荐:2人
  1004. `;
  1005. this.snackBar.open(reportSummary, '查看详细报告', {
  1006. duration: 6000,
  1007. horizontalPosition: 'center',
  1008. verticalPosition: 'top',
  1009. panelClass: ['info-snackbar']
  1010. }).onAction().subscribe(() => {
  1011. // 这里可以打开详细的试用期报告页面
  1012. console.log('打开试用期详细报告');
  1013. this.showProbationDetailReport();
  1014. });
  1015. }
  1016. private showProbationDetailReport(): void {
  1017. // 显示详细的试用期报告
  1018. this.snackBar.open('试用期详细报告功能开发中,敬请期待!', '关闭', {
  1019. duration: 3000,
  1020. horizontalPosition: 'center',
  1021. verticalPosition: 'top',
  1022. panelClass: ['warning-snackbar']
  1023. });
  1024. }
  1025. // 滑动功能相关属性和方法
  1026. currentScrollPosition = 0;
  1027. maxScrollPosition = 0;
  1028. scrollIndicatorDots: number[] = [];
  1029. initScrollIndicator(): void {
  1030. // 计算滚动指示器点数
  1031. const container = document.querySelector('.stages-timeline-container');
  1032. const timeline = document.querySelector('.stages-timeline');
  1033. if (container && timeline) {
  1034. const containerHeight = container.clientHeight;
  1035. const timelineHeight = timeline.scrollHeight;
  1036. if (timelineHeight > containerHeight) {
  1037. this.maxScrollPosition = timelineHeight - containerHeight;
  1038. const dotsCount = Math.ceil(timelineHeight / containerHeight);
  1039. this.scrollIndicatorDots = Array.from({ length: dotsCount }, (_, i) => i);
  1040. }
  1041. }
  1042. }
  1043. onTimelineScroll(event: Event): void {
  1044. const target = event.target as HTMLElement;
  1045. this.currentScrollPosition = target.scrollTop;
  1046. this.updateScrollIndicator();
  1047. }
  1048. updateScrollIndicator(): void {
  1049. const container = document.querySelector('.stages-timeline-container');
  1050. if (container) {
  1051. const scrollPercentage = this.currentScrollPosition / this.maxScrollPosition;
  1052. const activeIndex = Math.floor(scrollPercentage * this.scrollIndicatorDots.length);
  1053. // 更新指示器状态
  1054. document.querySelectorAll('.scroll-dot').forEach((dot, index) => {
  1055. if (index <= activeIndex) {
  1056. dot.classList.add('active');
  1057. } else {
  1058. dot.classList.remove('active');
  1059. }
  1060. });
  1061. }
  1062. }
  1063. scrollToPosition(index: number): void {
  1064. const container = document.querySelector('.stages-timeline-container');
  1065. if (container) {
  1066. const scrollPosition = (index / this.scrollIndicatorDots.length) * this.maxScrollPosition;
  1067. container.scrollTo({
  1068. top: scrollPosition,
  1069. behavior: 'smooth'
  1070. });
  1071. }
  1072. }
  1073. // 绩效筛选相关方法
  1074. onDepartmentChange(event: any): void {
  1075. console.log('部门筛选变更:', event.value);
  1076. this.selectedDepartment = event.value;
  1077. }
  1078. onLevelChange(event: any): void {
  1079. console.log('职级筛选变更:', event.value);
  1080. this.selectedLevel = event.value;
  1081. }
  1082. onTimeRangeChange(event: any): void {
  1083. console.log('时间范围变更:', event.value);
  1084. this.selectedTimeRange = event.value;
  1085. }
  1086. resetFilters(): void {
  1087. this.selectedDepartment = '';
  1088. this.selectedLevel = '';
  1089. this.selectedTimeRange = 'month';
  1090. this.quickFilter = '';
  1091. console.log('重置筛选条件');
  1092. this.applyFilters();
  1093. }
  1094. exportData(): void {
  1095. console.log('导出数据');
  1096. // 这里可以实现数据导出功能
  1097. }
  1098. applyQuickFilter(filterType: string): void {
  1099. this.quickFilter = this.quickFilter === filterType ? '' : filterType;
  1100. console.log('应用快速筛选:', filterType);
  1101. this.applyFilters();
  1102. }
  1103. private showUploadError(message: string) {
  1104. this.snackBar.open(message, '关闭', {
  1105. duration: 3000,
  1106. panelClass: ['error-snackbar']
  1107. });
  1108. }
  1109. private updateAnalysisProgress(progress: number) {
  1110. this.analysisProgress = progress;
  1111. }
  1112. private updateAnalysisResults(response: ResumeAnalysisResponse) {
  1113. // 更新匹配维度
  1114. this.matchDimensions = response.matchDimensions.map(dim => ({
  1115. id: dim.id,
  1116. name: dim.name,
  1117. score: dim.score,
  1118. level: dim.score >= 80 ? 'high' : dim.score >= 60 ? 'medium' : 'low',
  1119. icon: this.getSkillIcon(dim.name)
  1120. }));
  1121. // 更新推荐结论
  1122. this.recommendation = {
  1123. title: response.recommendation.title,
  1124. level: response.recommendation.level,
  1125. levelText: this.getRecommendationLevelText(response.recommendation.level),
  1126. icon: this.getRecommendationIcon(response.recommendation.level),
  1127. summary: response.recommendation.summary,
  1128. reasons: response.recommendation.reasons,
  1129. concerns: response.recommendation.concerns
  1130. };
  1131. // 更新筛选信息
  1132. this.screeningInfo = response.screeningInfo.map(info => ({
  1133. id: info.id,
  1134. title: info.title,
  1135. detail: info.detail,
  1136. status: info.status,
  1137. statusText: this.getStatusText(info.status),
  1138. icon: this.getScreeningIcon(info.title)
  1139. }));
  1140. this.showAnalysisResults = true;
  1141. }
  1142. private getSkillIcon(skillName: string): string {
  1143. const iconMap: { [key: string]: string } = {
  1144. '建模经验': 'view_in_ar',
  1145. 'UI设计': 'design_services',
  1146. '用户体验': 'psychology',
  1147. '团队协作': 'groups',
  1148. '项目管理': 'task_alt',
  1149. '技术能力': 'code',
  1150. '沟通能力': 'chat',
  1151. '学习能力': 'school'
  1152. };
  1153. return iconMap[skillName] || 'star';
  1154. }
  1155. private getRecommendationLevelText(level: string): string {
  1156. const levelMap: { [key: string]: string } = {
  1157. 'recommend': '推荐',
  1158. 'consider': '考虑',
  1159. 'reject': '不推荐'
  1160. };
  1161. return levelMap[level] || '待定';
  1162. }
  1163. private getRecommendationIcon(level: string): string {
  1164. const iconMap: { [key: string]: string } = {
  1165. 'recommend': 'thumb_up',
  1166. 'consider': 'help',
  1167. 'reject': 'thumb_down'
  1168. };
  1169. return iconMap[level] || 'help';
  1170. }
  1171. private getStatusText(status: string): string {
  1172. const statusMap: { [key: string]: string } = {
  1173. 'pass': '符合',
  1174. 'warning': '注意',
  1175. 'fail': '不符合'
  1176. };
  1177. return statusMap[status] || '未知';
  1178. }
  1179. private getScreeningIcon(title: string): string {
  1180. const iconMap: { [key: string]: string } = {
  1181. '学历要求': 'school',
  1182. '工作经验': 'work',
  1183. '技能匹配': 'star',
  1184. '薪资期望': 'payments',
  1185. '到岗时间': 'schedule'
  1186. };
  1187. return iconMap[title] || 'info';
  1188. }
  1189. // 显示上传反馈
  1190. showUploadFeedback(fileName: string) {
  1191. // 创建临时反馈元素
  1192. const feedback = document.createElement('div');
  1193. feedback.className = 'upload-feedback';
  1194. feedback.innerHTML = `
  1195. <mat-icon>check_circle</mat-icon>
  1196. <span>简历 "${fileName}" 上传成功!</span>
  1197. `;
  1198. feedback.style.cssText = `
  1199. position: fixed;
  1200. top: 20px;
  1201. right: 20px;
  1202. background: #4CAF50;
  1203. color: white;
  1204. padding: 12px 20px;
  1205. border-radius: 8px;
  1206. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  1207. z-index: 1000;
  1208. display: flex;
  1209. align-items: center;
  1210. gap: 8px;
  1211. animation: slideInRight 0.3s ease-out;
  1212. `;
  1213. document.body.appendChild(feedback);
  1214. // 3秒后移除反馈
  1215. setTimeout(() => {
  1216. feedback.style.animation = 'slideOutRight 0.3s ease-in';
  1217. setTimeout(() => {
  1218. document.body.removeChild(feedback);
  1219. }, 300);
  1220. }, 3000);
  1221. }
  1222. // 绩效相关方法
  1223. applyFilters() {
  1224. // 显示加载状态
  1225. this.isFilterLoading = true;
  1226. // 模拟筛选过程
  1227. setTimeout(() => {
  1228. this.isFilterLoading = false;
  1229. // 应用筛选条件
  1230. console.log('应用筛选条件:', {
  1231. department: this.selectedDepartment,
  1232. timeRange: this.selectedTimeRange
  1233. });
  1234. // 显示筛选成功反馈
  1235. this.showFilterFeedback();
  1236. }, 1000);
  1237. }
  1238. // 显示筛选反馈
  1239. showFilterFeedback() {
  1240. const feedback = document.createElement('div');
  1241. feedback.className = 'filter-feedback';
  1242. feedback.innerHTML = `
  1243. <mat-icon>filter_list</mat-icon>
  1244. <span>筛选条件已应用</span>
  1245. `;
  1246. feedback.style.cssText = `
  1247. position: fixed;
  1248. top: 20px;
  1249. right: 20px;
  1250. background: #2196F3;
  1251. color: white;
  1252. padding: 12px 20px;
  1253. border-radius: 8px;
  1254. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  1255. z-index: 1000;
  1256. display: flex;
  1257. align-items: center;
  1258. gap: 8px;
  1259. animation: slideInRight 0.3s ease-out;
  1260. `;
  1261. document.body.appendChild(feedback);
  1262. setTimeout(() => {
  1263. feedback.style.animation = 'slideOutRight 0.3s ease-in';
  1264. setTimeout(() => {
  1265. document.body.removeChild(feedback);
  1266. }, 300);
  1267. }, 2000);
  1268. }
  1269. // 初始化图表(这里需要后续集成ECharts)
  1270. private initCharts() {
  1271. // 初始化离职原因图表
  1272. this.initResignationChart();
  1273. // 初始化对比图表
  1274. this.initComparisonChart();
  1275. }
  1276. // 初始化离职原因图表
  1277. private initResignationChart() {
  1278. if (!this.resignationChartRef?.nativeElement) return;
  1279. const ctx = this.resignationChartRef.nativeElement.getContext('2d');
  1280. if (!ctx) return;
  1281. // 销毁现有图表
  1282. if (this.resignationChart) {
  1283. this.resignationChart.destroy();
  1284. }
  1285. const chartData = {
  1286. labels: this.resignationReasons.map(reason => reason.name),
  1287. datasets: [{
  1288. data: this.resignationReasons.map(reason => reason.percentage),
  1289. backgroundColor: [
  1290. '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
  1291. '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'
  1292. ],
  1293. borderWidth: 2,
  1294. borderColor: '#fff'
  1295. }]
  1296. };
  1297. let config: ChartConfiguration;
  1298. // 通用动画配置
  1299. const animationConfig = {
  1300. duration: 800,
  1301. easing: 'easeInOutQuart' as const,
  1302. delay: (context: any) => context.dataIndex * 50
  1303. };
  1304. switch (this.reasonsChartType) {
  1305. case 'pie':
  1306. config = {
  1307. type: 'pie',
  1308. data: chartData,
  1309. options: {
  1310. responsive: true,
  1311. maintainAspectRatio: false,
  1312. animation: animationConfig,
  1313. plugins: {
  1314. legend: {
  1315. position: 'right',
  1316. labels: {
  1317. usePointStyle: true,
  1318. padding: 20,
  1319. font: {
  1320. size: 12
  1321. }
  1322. }
  1323. },
  1324. tooltip: {
  1325. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  1326. titleColor: '#fff',
  1327. bodyColor: '#fff',
  1328. borderColor: '#4ECDC4',
  1329. borderWidth: 1,
  1330. callbacks: {
  1331. label: (context) => {
  1332. const label = context.label || '';
  1333. const value = context.parsed;
  1334. const reason = this.resignationReasons[context.dataIndex];
  1335. return `${label}: ${value}% (${reason.count}人)`;
  1336. }
  1337. }
  1338. }
  1339. }
  1340. }
  1341. };
  1342. break;
  1343. case 'doughnut':
  1344. config = {
  1345. type: 'doughnut',
  1346. data: chartData,
  1347. options: {
  1348. responsive: true,
  1349. maintainAspectRatio: false,
  1350. animation: animationConfig,
  1351. plugins: {
  1352. legend: {
  1353. position: 'right',
  1354. labels: {
  1355. usePointStyle: true,
  1356. padding: 20,
  1357. font: {
  1358. size: 12
  1359. }
  1360. }
  1361. },
  1362. tooltip: {
  1363. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  1364. titleColor: '#fff',
  1365. bodyColor: '#fff',
  1366. borderColor: '#4ECDC4',
  1367. borderWidth: 1,
  1368. callbacks: {
  1369. label: (context) => {
  1370. const label = context.label || '';
  1371. const value = context.parsed;
  1372. const reason = this.resignationReasons[context.dataIndex];
  1373. return `${label}: ${value}% (${reason.count}人)`;
  1374. }
  1375. }
  1376. }
  1377. }
  1378. }
  1379. };
  1380. break;
  1381. case 'bar':
  1382. config = {
  1383. type: 'bar',
  1384. data: {
  1385. labels: this.resignationReasons.map(reason => reason.name),
  1386. datasets: [{
  1387. label: '离职占比 (%)',
  1388. data: this.resignationReasons.map(reason => reason.percentage),
  1389. backgroundColor: '#4ECDC4',
  1390. borderColor: '#45B7D1',
  1391. borderWidth: 1,
  1392. borderRadius: 4,
  1393. borderSkipped: false
  1394. }]
  1395. },
  1396. options: {
  1397. responsive: true,
  1398. maintainAspectRatio: false,
  1399. animation: {
  1400. duration: 800,
  1401. easing: 'easeInOutQuart' as const,
  1402. delay: (context: any) => context.dataIndex * 100
  1403. },
  1404. plugins: {
  1405. legend: {
  1406. display: false
  1407. },
  1408. tooltip: {
  1409. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  1410. titleColor: '#fff',
  1411. bodyColor: '#fff',
  1412. borderColor: '#4ECDC4',
  1413. borderWidth: 1,
  1414. callbacks: {
  1415. label: (context) => {
  1416. const value = context.parsed.y;
  1417. const reason = this.resignationReasons[context.dataIndex];
  1418. return `${reason.name}: ${value}% (${reason.count}人)`;
  1419. }
  1420. }
  1421. }
  1422. },
  1423. scales: {
  1424. y: {
  1425. beginAtZero: true,
  1426. max: 35,
  1427. grid: {
  1428. color: 'rgba(0, 0, 0, 0.1)'
  1429. },
  1430. ticks: {
  1431. font: {
  1432. size: 11
  1433. },
  1434. callback: function(value) {
  1435. return value + '%';
  1436. }
  1437. }
  1438. },
  1439. x: {
  1440. grid: {
  1441. display: false
  1442. },
  1443. ticks: {
  1444. maxRotation: 45,
  1445. minRotation: 0,
  1446. font: {
  1447. size: 11
  1448. }
  1449. }
  1450. }
  1451. }
  1452. }
  1453. };
  1454. break;
  1455. default:
  1456. return;
  1457. }
  1458. this.resignationChart = new Chart(ctx, config);
  1459. }
  1460. // 切换图表类型 - 优化性能
  1461. onChartTypeChange() {
  1462. // 添加加载状态
  1463. const chartContainer = this.resignationChartRef?.nativeElement?.parentElement;
  1464. if (chartContainer) {
  1465. chartContainer.style.opacity = '0.7';
  1466. chartContainer.style.transition = 'opacity 0.3s ease';
  1467. }
  1468. // 使用 setTimeout 确保 UI 更新
  1469. setTimeout(() => {
  1470. this.initResignationChart();
  1471. // 恢复透明度
  1472. if (chartContainer) {
  1473. setTimeout(() => {
  1474. chartContainer.style.opacity = '1';
  1475. }, 100);
  1476. }
  1477. }, 50);
  1478. }
  1479. // 初始化对比图表
  1480. private initComparisonChart() {
  1481. if (!this.comparisonChartRef?.nativeElement) {
  1482. return;
  1483. }
  1484. const ctx = this.comparisonChartRef.nativeElement.getContext('2d');
  1485. if (!ctx) return;
  1486. // 销毁现有图表
  1487. if (this.comparisonChart) {
  1488. this.comparisonChart.destroy();
  1489. }
  1490. // 根据图表类型创建不同的配置
  1491. let config: ChartConfiguration;
  1492. if (this.comparisonChartType === 'bar') {
  1493. config = {
  1494. type: 'bar',
  1495. data: {
  1496. labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
  1497. datasets: [
  1498. {
  1499. label: 'UI设计部',
  1500. data: [85, 88, 92, 89, 91, 94],
  1501. backgroundColor: 'rgba(76, 175, 80, 0.8)',
  1502. borderColor: 'rgba(76, 175, 80, 1)',
  1503. borderWidth: 1
  1504. },
  1505. {
  1506. label: '3D建模部',
  1507. data: [78, 82, 85, 87, 84, 89],
  1508. backgroundColor: 'rgba(33, 150, 243, 0.8)',
  1509. borderColor: 'rgba(33, 150, 243, 1)',
  1510. borderWidth: 1
  1511. },
  1512. {
  1513. label: '前端开发部',
  1514. data: [82, 85, 88, 86, 90, 92],
  1515. backgroundColor: 'rgba(255, 152, 0, 0.8)',
  1516. borderColor: 'rgba(255, 152, 0, 1)',
  1517. borderWidth: 1
  1518. }
  1519. ]
  1520. },
  1521. options: {
  1522. responsive: true,
  1523. maintainAspectRatio: false,
  1524. plugins: {
  1525. legend: {
  1526. position: 'top',
  1527. labels: {
  1528. usePointStyle: true,
  1529. padding: 20
  1530. }
  1531. },
  1532. tooltip: {
  1533. mode: 'index',
  1534. intersect: false,
  1535. callbacks: {
  1536. label: (context) => {
  1537. return `${context.dataset.label}: ${context.parsed.y}%`;
  1538. }
  1539. }
  1540. }
  1541. },
  1542. scales: {
  1543. x: {
  1544. grid: {
  1545. display: false
  1546. }
  1547. },
  1548. y: {
  1549. beginAtZero: true,
  1550. max: 100,
  1551. ticks: {
  1552. callback: (value) => `${value}%`
  1553. }
  1554. }
  1555. }
  1556. }
  1557. };
  1558. } else if (this.comparisonChartType === 'line') {
  1559. config = {
  1560. type: 'line',
  1561. data: {
  1562. labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
  1563. datasets: [
  1564. {
  1565. label: 'UI设计部',
  1566. data: [85, 88, 92, 89, 91, 94],
  1567. borderColor: 'rgba(76, 175, 80, 1)',
  1568. backgroundColor: 'rgba(76, 175, 80, 0.1)',
  1569. borderWidth: 3,
  1570. fill: true,
  1571. tension: 0.4,
  1572. pointBackgroundColor: 'rgba(76, 175, 80, 1)',
  1573. pointBorderColor: '#fff',
  1574. pointBorderWidth: 2,
  1575. pointRadius: 6
  1576. },
  1577. {
  1578. label: '3D建模部',
  1579. data: [78, 82, 85, 87, 84, 89],
  1580. borderColor: 'rgba(33, 150, 243, 1)',
  1581. backgroundColor: 'rgba(33, 150, 243, 0.1)',
  1582. borderWidth: 3,
  1583. fill: true,
  1584. tension: 0.4,
  1585. pointBackgroundColor: 'rgba(33, 150, 243, 1)',
  1586. pointBorderColor: '#fff',
  1587. pointBorderWidth: 2,
  1588. pointRadius: 6
  1589. },
  1590. {
  1591. label: '前端开发部',
  1592. data: [82, 85, 88, 86, 90, 92],
  1593. borderColor: 'rgba(255, 152, 0, 1)',
  1594. backgroundColor: 'rgba(255, 152, 0, 0.1)',
  1595. borderWidth: 3,
  1596. fill: true,
  1597. tension: 0.4,
  1598. pointBackgroundColor: 'rgba(255, 152, 0, 1)',
  1599. pointBorderColor: '#fff',
  1600. pointBorderWidth: 2,
  1601. pointRadius: 6
  1602. }
  1603. ]
  1604. },
  1605. options: {
  1606. responsive: true,
  1607. maintainAspectRatio: false,
  1608. plugins: {
  1609. legend: {
  1610. position: 'top',
  1611. labels: {
  1612. usePointStyle: true,
  1613. padding: 20
  1614. }
  1615. },
  1616. tooltip: {
  1617. mode: 'index',
  1618. intersect: false,
  1619. callbacks: {
  1620. label: (context) => {
  1621. return `${context.dataset.label}: ${context.parsed.y}%`;
  1622. }
  1623. }
  1624. }
  1625. },
  1626. scales: {
  1627. x: {
  1628. grid: {
  1629. display: false
  1630. }
  1631. },
  1632. y: {
  1633. beginAtZero: true,
  1634. max: 100,
  1635. ticks: {
  1636. callback: (value) => `${value}%`
  1637. }
  1638. }
  1639. },
  1640. interaction: {
  1641. mode: 'nearest',
  1642. axis: 'x',
  1643. intersect: false
  1644. }
  1645. }
  1646. };
  1647. } else { // radar
  1648. config = {
  1649. type: 'radar',
  1650. data: {
  1651. labels: ['完成率', '优秀率', '满意度', '按时率', '创新度', '协作度'],
  1652. datasets: [
  1653. {
  1654. label: 'UI设计部',
  1655. data: [92, 78, 88, 92, 85, 90],
  1656. borderColor: 'rgba(76, 175, 80, 1)',
  1657. backgroundColor: 'rgba(76, 175, 80, 0.2)',
  1658. borderWidth: 2,
  1659. pointBackgroundColor: 'rgba(76, 175, 80, 1)',
  1660. pointBorderColor: '#fff',
  1661. pointBorderWidth: 2
  1662. },
  1663. {
  1664. label: '3D建模部',
  1665. data: [85, 82, 90, 88, 92, 85],
  1666. borderColor: 'rgba(33, 150, 243, 1)',
  1667. backgroundColor: 'rgba(33, 150, 243, 0.2)',
  1668. borderWidth: 2,
  1669. pointBackgroundColor: 'rgba(33, 150, 243, 1)',
  1670. pointBorderColor: '#fff',
  1671. pointBorderWidth: 2
  1672. },
  1673. {
  1674. label: '前端开发部',
  1675. data: [88, 75, 85, 88, 88, 92],
  1676. borderColor: 'rgba(255, 152, 0, 1)',
  1677. backgroundColor: 'rgba(255, 152, 0, 0.2)',
  1678. borderWidth: 2,
  1679. pointBackgroundColor: 'rgba(255, 152, 0, 1)',
  1680. pointBorderColor: '#fff',
  1681. pointBorderWidth: 2
  1682. }
  1683. ]
  1684. },
  1685. options: {
  1686. responsive: true,
  1687. maintainAspectRatio: false,
  1688. plugins: {
  1689. legend: {
  1690. position: 'top',
  1691. labels: {
  1692. usePointStyle: true,
  1693. padding: 20
  1694. }
  1695. }
  1696. },
  1697. scales: {
  1698. r: {
  1699. beginAtZero: true,
  1700. max: 100,
  1701. ticks: {
  1702. stepSize: 20,
  1703. callback: (value) => `${value}%`
  1704. },
  1705. grid: {
  1706. color: 'rgba(0, 0, 0, 0.1)'
  1707. },
  1708. angleLines: {
  1709. color: 'rgba(0, 0, 0, 0.1)'
  1710. }
  1711. }
  1712. }
  1713. }
  1714. };
  1715. }
  1716. this.comparisonChart = new Chart(ctx, config);
  1717. }
  1718. // 切换对比图表类型
  1719. onComparisonChartTypeChange() {
  1720. // 添加加载状态
  1721. const chartContainer = this.comparisonChartRef?.nativeElement?.parentElement;
  1722. if (chartContainer) {
  1723. chartContainer.style.opacity = '0.7';
  1724. chartContainer.style.transition = 'opacity 0.3s ease';
  1725. }
  1726. // 使用 setTimeout 确保 UI 更新
  1727. setTimeout(() => {
  1728. this.initComparisonChart();
  1729. // 恢复透明度
  1730. if (chartContainer) {
  1731. setTimeout(() => {
  1732. chartContainer.style.opacity = '1';
  1733. }, 100);
  1734. }
  1735. }, 50);
  1736. }
  1737. // 拖拽排序
  1738. drop(event: CdkDragDrop<TodoItem[]>) {
  1739. moveItemInArray(this.todoItems, event.previousIndex, event.currentIndex);
  1740. }
  1741. // 获取优先级颜色
  1742. getPriorityColor(priority: string): string {
  1743. switch (priority) {
  1744. case 'high': return '#ff4757';
  1745. case 'medium': return '#ffa502';
  1746. case 'low': return '#2ed573';
  1747. default: return '#a4b0be';
  1748. }
  1749. }
  1750. // 获取空缺岗位图标
  1751. getVacancyIcon(urgency: string): string {
  1752. switch (urgency) {
  1753. case 'urgent': return 'warning';
  1754. case 'normal': return 'info';
  1755. default: return 'help';
  1756. }
  1757. }
  1758. // 获取部门颜色
  1759. getDepartmentColor(department: string): string {
  1760. const colors: { [key: string]: string } = {
  1761. 'UI设计部': '#2196F3',
  1762. '3D建模部': '#4CAF50',
  1763. '前端开发部': '#FF9800',
  1764. '产品部': '#9C27B0'
  1765. };
  1766. return colors[department] || '#757575';
  1767. }
  1768. // 获取优先级类名
  1769. getPriorityClass(priority: string): string {
  1770. return `priority-${priority}`;
  1771. }
  1772. toggleTodoList(): void {
  1773. this.showTodoList = !this.showTodoList;
  1774. }
  1775. // 悬浮待办事项面板相关属性和方法
  1776. isTodoPanelOpen: boolean = false;
  1777. get todoCount(): number {
  1778. return this.todoList.length;
  1779. }
  1780. toggleTodoPanel(): void {
  1781. this.isTodoPanelOpen = !this.isTodoPanelOpen;
  1782. }
  1783. // 获取甜甜圈图表特定选项
  1784. private getDoughnutOptions(): any {
  1785. return {
  1786. cutout: '60%'
  1787. };
  1788. }
  1789. // 初始化所有图表
  1790. private initializeCharts() {
  1791. this.initPieChart();
  1792. this.initLineChart();
  1793. this.initRadarChart();
  1794. this.initResignationChart();
  1795. this.initComparisonChart();
  1796. }
  1797. // 初始化职级分布饼图
  1798. private initPieChart() {
  1799. if (!this.pieChartRef?.nativeElement) return;
  1800. const ctx = this.pieChartRef.nativeElement.getContext('2d');
  1801. if (!ctx) return;
  1802. const config: ChartConfiguration = {
  1803. type: 'doughnut',
  1804. data: {
  1805. labels: this.rankDistribution.map(item => item.level),
  1806. datasets: [{
  1807. data: this.rankDistribution.map(item => item.percentage),
  1808. backgroundColor: this.rankDistribution.map(item => item.color),
  1809. borderWidth: 0,
  1810. hoverBorderWidth: 2,
  1811. hoverBorderColor: '#fff'
  1812. }]
  1813. },
  1814. options: {
  1815. responsive: true,
  1816. maintainAspectRatio: false,
  1817. plugins: {
  1818. legend: {
  1819. display: false
  1820. },
  1821. tooltip: {
  1822. callbacks: {
  1823. label: (context) => {
  1824. const label = context.label || '';
  1825. const value = context.parsed;
  1826. const count = this.rankDistribution[context.dataIndex].count;
  1827. return `${label}: ${value}% (${count}人)`;
  1828. }
  1829. }
  1830. }
  1831. },
  1832. ...(this.getDoughnutOptions())
  1833. }
  1834. };
  1835. this.pieChart = new Chart(ctx, config);
  1836. }
  1837. // 初始化入职离职趋势折线图
  1838. private initLineChart() {
  1839. if (!this.lineChartRef?.nativeElement) return;
  1840. const ctx = this.lineChartRef.nativeElement.getContext('2d');
  1841. if (!ctx) return;
  1842. const config: ChartConfiguration = {
  1843. type: 'line',
  1844. data: {
  1845. labels: this.monthlyHireData.map(item => item.month),
  1846. datasets: [
  1847. {
  1848. label: '入职人数',
  1849. data: this.monthlyHireData.map(item => item.hired),
  1850. borderColor: '#4CAF50',
  1851. backgroundColor: 'rgba(76, 175, 80, 0.1)',
  1852. tension: 0.4,
  1853. fill: true
  1854. },
  1855. {
  1856. label: '离职人数',
  1857. data: this.monthlyHireData.map(item => item.left),
  1858. borderColor: '#f44336',
  1859. backgroundColor: 'rgba(244, 67, 54, 0.1)',
  1860. tension: 0.4,
  1861. fill: true
  1862. }
  1863. ]
  1864. },
  1865. options: {
  1866. responsive: true,
  1867. maintainAspectRatio: false,
  1868. plugins: {
  1869. legend: {
  1870. position: 'top'
  1871. },
  1872. tooltip: {
  1873. mode: 'index',
  1874. intersect: false
  1875. }
  1876. },
  1877. scales: {
  1878. y: {
  1879. beginAtZero: true,
  1880. grid: {
  1881. color: 'rgba(0, 0, 0, 0.1)'
  1882. }
  1883. },
  1884. x: {
  1885. grid: {
  1886. color: 'rgba(0, 0, 0, 0.1)'
  1887. }
  1888. }
  1889. },
  1890. interaction: {
  1891. mode: 'nearest',
  1892. axis: 'x',
  1893. intersect: false
  1894. }
  1895. }
  1896. };
  1897. this.lineChart = new Chart(ctx, config);
  1898. }
  1899. // 初始化绩效总览雷达图
  1900. private initRadarChart() {
  1901. if (!this.radarChartRef?.nativeElement) return;
  1902. const ctx = this.radarChartRef.nativeElement.getContext('2d');
  1903. if (!ctx) return;
  1904. const config: ChartConfiguration = {
  1905. type: 'radar',
  1906. data: {
  1907. labels: ['项目完成率', '优秀作品率', '客户满意度', '逾期率'],
  1908. datasets: this.departmentPerformance.map((dept, index) => ({
  1909. label: dept.department,
  1910. data: [
  1911. dept.completionRate,
  1912. dept.excellentWorkRate,
  1913. dept.satisfactionRate,
  1914. 100 - dept.overdueRate // 逾期率转换为正向指标
  1915. ],
  1916. borderColor: this.getDepartmentColor(dept.department),
  1917. backgroundColor: this.getDepartmentColor(dept.department) + '20',
  1918. pointBackgroundColor: this.getDepartmentColor(dept.department),
  1919. pointBorderColor: '#fff',
  1920. pointHoverBackgroundColor: '#fff',
  1921. pointHoverBorderColor: this.getDepartmentColor(dept.department)
  1922. }))
  1923. },
  1924. options: {
  1925. responsive: true,
  1926. maintainAspectRatio: false,
  1927. plugins: {
  1928. legend: {
  1929. position: 'bottom'
  1930. }
  1931. },
  1932. scales: {
  1933. r: {
  1934. beginAtZero: true,
  1935. max: 100,
  1936. grid: {
  1937. color: 'rgba(0, 0, 0, 0.1)'
  1938. },
  1939. angleLines: {
  1940. color: 'rgba(0, 0, 0, 0.1)'
  1941. },
  1942. pointLabels: {
  1943. font: {
  1944. size: 12
  1945. }
  1946. }
  1947. }
  1948. }
  1949. }
  1950. };
  1951. this.radarChart = new Chart(ctx, config);
  1952. }
  1953. getPriorityLabel(priority: string): string {
  1954. switch (priority) {
  1955. case 'high': return '紧急';
  1956. case 'medium': return '重要';
  1957. case 'low': return '一般';
  1958. default: return '未知';
  1959. }
  1960. }
  1961. getTypeLabel(type: string): string {
  1962. switch (type) {
  1963. case 'resume': return '简历筛选';
  1964. case 'onboarding': return '入职跟进';
  1965. case 'resignation': return '离职处理';
  1966. default: return type;
  1967. }
  1968. }
  1969. getStatusLabel(status: string): string {
  1970. const statusMap: { [key: string]: string } = {
  1971. 'pending': '待处理',
  1972. 'in_progress': '进行中',
  1973. 'completed': '已完成',
  1974. 'urgent': '紧急',
  1975. 'normal': '普通'
  1976. };
  1977. return statusMap[status] || status;
  1978. }
  1979. // 拖拽排序功能
  1980. onTodoDrop(event: CdkDragDrop<TodoItem[]>): void {
  1981. if (event.previousIndex !== event.currentIndex) {
  1982. moveItemInArray(this.todoItems, event.previousIndex, event.currentIndex);
  1983. this.showTodoDragFeedback();
  1984. }
  1985. }
  1986. // 更新待办事项状态
  1987. updateTodoStatus(todo: TodoItem, status: 'pending' | 'completed' | 'in_progress'): void {
  1988. const oldStatus = todo.status;
  1989. todo.status = status;
  1990. this.showTodoStatusFeedback(todo.title, oldStatus, status);
  1991. }
  1992. private showTodoDragFeedback(): void {
  1993. // 创建反馈元素
  1994. const feedback = document.createElement('div');
  1995. feedback.className = 'ios-drag-feedback';
  1996. feedback.innerHTML = `
  1997. <div class="ios-feedback-content">
  1998. <mat-icon>swap_vert</mat-icon>
  1999. <span>任务顺序已更新</span>
  2000. </div>
  2001. `;
  2002. // 添加样式
  2003. feedback.style.cssText = `
  2004. position: fixed;
  2005. top: 50%;
  2006. left: 50%;
  2007. transform: translate(-50%, -50%);
  2008. background: rgba(0, 122, 255, 0.95);
  2009. color: white;
  2010. padding: 12px 20px;
  2011. border-radius: 12px;
  2012. box-shadow: 0 8px 32px rgba(0, 122, 255, 0.3);
  2013. z-index: 10000;
  2014. display: flex;
  2015. align-items: center;
  2016. gap: 8px;
  2017. font-size: 14px;
  2018. font-weight: 500;
  2019. backdrop-filter: blur(20px);
  2020. animation: iosFeedbackIn 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  2021. `;
  2022. document.body.appendChild(feedback);
  2023. // 2秒后移除
  2024. setTimeout(() => {
  2025. feedback.style.animation = 'iosFeedbackOut 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
  2026. setTimeout(() => {
  2027. if (feedback.parentNode) {
  2028. feedback.parentNode.removeChild(feedback);
  2029. }
  2030. }, 300);
  2031. }, 2000);
  2032. }
  2033. private showTodoStatusFeedback(title: string, oldStatus: string, newStatus: string): void {
  2034. const statusMap = {
  2035. 'pending': '待处理',
  2036. 'in_progress': '进行中',
  2037. 'completed': '已完成'
  2038. };
  2039. const iconMap = {
  2040. 'pending': 'schedule',
  2041. 'in_progress': 'play_circle',
  2042. 'completed': 'check_circle'
  2043. };
  2044. const colorMap = {
  2045. 'pending': '#8E8E93',
  2046. 'in_progress': '#FF9500',
  2047. 'completed': '#34C759'
  2048. };
  2049. // 创建反馈元素
  2050. const feedback = document.createElement('div');
  2051. feedback.className = 'ios-status-feedback';
  2052. feedback.innerHTML = `
  2053. <div class="ios-feedback-content">
  2054. <mat-icon style="color: ${colorMap[newStatus as keyof typeof colorMap]}">${iconMap[newStatus as keyof typeof iconMap]}</mat-icon>
  2055. <div class="ios-feedback-text">
  2056. <div class="ios-feedback-title">${title}</div>
  2057. <div class="ios-feedback-subtitle">${statusMap[oldStatus as keyof typeof statusMap]} → ${statusMap[newStatus as keyof typeof statusMap]}</div>
  2058. </div>
  2059. </div>
  2060. `;
  2061. // 添加样式
  2062. feedback.style.cssText = `
  2063. position: fixed;
  2064. top: 50%;
  2065. left: 50%;
  2066. transform: translate(-50%, -50%);
  2067. background: rgba(255, 255, 255, 0.95);
  2068. color: #1D1D1F;
  2069. padding: 16px 20px;
  2070. border-radius: 16px;
  2071. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
  2072. z-index: 10000;
  2073. backdrop-filter: blur(20px);
  2074. animation: iosFeedbackIn 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  2075. min-width: 280px;
  2076. `;
  2077. // 添加内部样式
  2078. const style = document.createElement('style');
  2079. style.textContent = `
  2080. .ios-feedback-content {
  2081. display: flex;
  2082. align-items: center;
  2083. gap: 12px;
  2084. }
  2085. .ios-feedback-text {
  2086. flex: 1;
  2087. }
  2088. .ios-feedback-title {
  2089. font-size: 16px;
  2090. font-weight: 600;
  2091. margin-bottom: 4px;
  2092. }
  2093. .ios-feedback-subtitle {
  2094. font-size: 14px;
  2095. color: #8E8E93;
  2096. }
  2097. @keyframes iosFeedbackIn {
  2098. from {
  2099. opacity: 0;
  2100. transform: translate(-50%, -50%) scale(0.8);
  2101. }
  2102. to {
  2103. opacity: 1;
  2104. transform: translate(-50%, -50%) scale(1);
  2105. }
  2106. }
  2107. @keyframes iosFeedbackOut {
  2108. from {
  2109. opacity: 1;
  2110. transform: translate(-50%, -50%) scale(1);
  2111. }
  2112. to {
  2113. opacity: 0;
  2114. transform: translate(-50%, -50%) scale(0.8);
  2115. }
  2116. }
  2117. `;
  2118. document.head.appendChild(style);
  2119. document.body.appendChild(feedback);
  2120. // 2.5秒后移除
  2121. setTimeout(() => {
  2122. feedback.style.animation = 'iosFeedbackOut 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
  2123. setTimeout(() => {
  2124. if (feedback.parentNode) {
  2125. feedback.parentNode.removeChild(feedback);
  2126. }
  2127. if (style.parentNode) {
  2128. style.parentNode.removeChild(style);
  2129. }
  2130. }, 300);
  2131. }, 2500);
  2132. }
  2133. // 处理按钮按压效果
  2134. isButtonPressed = false;
  2135. handleButtonPress(action: 'press' | 'release') {
  2136. this.isButtonPressed = action === 'press';
  2137. }
  2138. // 处理检查点状态变化
  2139. onCheckpointChange(checkpoint: OnboardingCheckpoint, event: any) {
  2140. checkpoint.completed = event.checked;
  2141. // 显示状态变化反馈
  2142. this.showCheckpointFeedback(checkpoint.title, checkpoint.completed);
  2143. // 如果完成,添加完成动画效果
  2144. if (checkpoint.completed) {
  2145. this.animateCheckpointCompletion(checkpoint.id);
  2146. }
  2147. }
  2148. // 显示检查点反馈
  2149. showCheckpointFeedback(title: string, completed: boolean) {
  2150. const feedback = document.createElement('div');
  2151. feedback.className = 'checkpoint-feedback';
  2152. feedback.innerHTML = `
  2153. <mat-icon>${completed ? 'check_circle' : 'radio_button_unchecked'}</mat-icon>
  2154. <span>${completed ? '已完成' : '已取消'}: ${title}</span>
  2155. `;
  2156. feedback.style.cssText = `
  2157. position: fixed;
  2158. top: 20px;
  2159. right: 20px;
  2160. background: ${completed ? '#4CAF50' : '#FF9800'};
  2161. color: white;
  2162. padding: 12px 20px;
  2163. border-radius: 8px;
  2164. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  2165. z-index: 1000;
  2166. display: flex;
  2167. align-items: center;
  2168. gap: 8px;
  2169. animation: slideInRight 0.3s ease-out;
  2170. max-width: 300px;
  2171. `;
  2172. document.body.appendChild(feedback);
  2173. setTimeout(() => {
  2174. feedback.style.animation = 'slideOutRight 0.3s ease-in';
  2175. setTimeout(() => {
  2176. document.body.removeChild(feedback);
  2177. }, 300);
  2178. }, 2500);
  2179. }
  2180. // 检查点完成动画
  2181. animateCheckpointCompletion(checkpointId: number) {
  2182. const element = document.querySelector(`[data-checkpoint-id="${checkpointId}"]`);
  2183. if (element) {
  2184. element.classList.add('completed-animation');
  2185. setTimeout(() => {
  2186. element.classList.remove('completed-animation');
  2187. }, 600);
  2188. }
  2189. }
  2190. // 新人进度更新
  2191. updateNewbieProgress(newbieId: number, progress: number) {
  2192. const newbie = this.newbieList.find(n => n.id === newbieId);
  2193. if (newbie) {
  2194. newbie.progress = Math.min(100, Math.max(0, progress));
  2195. this.showProgressFeedback(newbie.name, newbie.progress);
  2196. }
  2197. }
  2198. // 显示进度反馈
  2199. showProgressFeedback(name: string, progress: number) {
  2200. const feedback = document.createElement('div');
  2201. feedback.className = 'progress-feedback';
  2202. feedback.innerHTML = `
  2203. <mat-icon>trending_up</mat-icon>
  2204. <span>${name} 的进度已更新至 ${progress}%</span>
  2205. `;
  2206. feedback.style.cssText = `
  2207. position: fixed;
  2208. top: 20px;
  2209. right: 20px;
  2210. background: #9C27B0;
  2211. color: white;
  2212. padding: 12px 20px;
  2213. border-radius: 8px;
  2214. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  2215. z-index: 1000;
  2216. display: flex;
  2217. align-items: center;
  2218. gap: 8px;
  2219. animation: slideInRight 0.3s ease-out;
  2220. `;
  2221. document.body.appendChild(feedback);
  2222. setTimeout(() => {
  2223. feedback.style.animation = 'slideOutRight 0.3s ease-in';
  2224. setTimeout(() => {
  2225. document.body.removeChild(feedback);
  2226. }, 300);
  2227. }, 2000);
  2228. }
  2229. // 绩效指标相关方法
  2230. refreshMetrics() {
  2231. console.log('刷新绩效指标数据');
  2232. // 模拟数据刷新
  2233. this.performanceMetrics.forEach(metric => {
  2234. // 随机更新数值以模拟实时数据
  2235. const currentValue = parseInt(metric.value);
  2236. const variation = Math.random() * 4 - 2; // -2 到 +2 的随机变化
  2237. const newValue = Math.max(0, Math.min(100, currentValue + variation));
  2238. metric.value = Math.round(newValue).toString();
  2239. // 更新进度条值
  2240. if (metric.id === 'overdue-rate') {
  2241. metric.progressValue = Math.max(0, 100 - newValue); // 逾期率反向显示
  2242. } else {
  2243. metric.progressValue = newValue;
  2244. }
  2245. });
  2246. this.showMetricsRefreshFeedback();
  2247. }
  2248. exportMetrics() {
  2249. console.log('导出绩效指标报告');
  2250. // 模拟导出功能
  2251. const reportData = {
  2252. exportTime: new Date().toISOString(),
  2253. metrics: this.performanceMetrics.map(metric => ({
  2254. title: metric.title,
  2255. value: metric.value + metric.unit,
  2256. target: metric.target,
  2257. achievement: metric.achievement,
  2258. trend: metric.trend.value + ' ' + metric.trend.label
  2259. }))
  2260. };
  2261. console.log('报告数据:', reportData);
  2262. this.showExportFeedback();
  2263. }
  2264. viewMetricDetails(metricId: string) {
  2265. console.log('查看指标详情:', metricId);
  2266. const metric = this.performanceMetrics.find(m => m.id === metricId);
  2267. if (metric) {
  2268. this.showMetricDetailsFeedback(metric);
  2269. }
  2270. }
  2271. viewMetricTrend(metricId: string) {
  2272. console.log('查看指标趋势:', metricId);
  2273. const metric = this.performanceMetrics.find(m => m.id === metricId);
  2274. if (metric) {
  2275. this.showMetricTrendFeedback(metric);
  2276. }
  2277. }
  2278. private showMetricDetailsFeedback(metric: PerformanceMetric) {
  2279. const feedback = document.createElement('div');
  2280. feedback.className = 'metric-details-feedback';
  2281. feedback.innerHTML = `
  2282. <div class="feedback-header">
  2283. <mat-icon>${metric.icon}</mat-icon>
  2284. <span>查看${metric.title}详情</span>
  2285. </div>
  2286. <div class="feedback-content">
  2287. <div class="metric-info">
  2288. <div class="info-item">
  2289. <span class="label">当前值:</span>
  2290. <span class="value">${metric.value}${metric.unit}</span>
  2291. </div>
  2292. <div class="info-item">
  2293. <span class="label">目标值:</span>
  2294. <span class="value">${metric.target}</span>
  2295. </div>
  2296. <div class="info-item">
  2297. <span class="label">完成率:</span>
  2298. <span class="value ${metric.achievementClass}">${metric.achievement}</span>
  2299. </div>
  2300. <div class="info-item">
  2301. <span class="label">趋势:</span>
  2302. <span class="value trend-${metric.trend.type}">
  2303. <mat-icon>${metric.trend.icon}</mat-icon>
  2304. ${metric.trend.value}
  2305. </span>
  2306. </div>
  2307. </div>
  2308. </div>
  2309. `;
  2310. feedback.style.cssText = `
  2311. position: fixed;
  2312. top: 50%;
  2313. left: 50%;
  2314. transform: translate(-50%, -50%);
  2315. background: white;
  2316. border-radius: 12px;
  2317. box-shadow: 0 8px 32px rgba(0,0,0,0.15);
  2318. padding: 24px;
  2319. z-index: 10000;
  2320. min-width: 400px;
  2321. max-width: 500px;
  2322. animation: slideInScale 0.3s ease-out;
  2323. `;
  2324. const style = document.createElement('style');
  2325. style.textContent = `
  2326. @keyframes slideInScale {
  2327. from {
  2328. opacity: 0;
  2329. transform: translate(-50%, -50%) scale(0.9);
  2330. }
  2331. to {
  2332. opacity: 1;
  2333. transform: translate(-50%, -50%) scale(1);
  2334. }
  2335. }
  2336. .metric-details-feedback .feedback-header {
  2337. display: flex;
  2338. align-items: center;
  2339. gap: 16px;
  2340. margin-bottom: 20px;
  2341. font-size: 18px;
  2342. font-weight: 600;
  2343. color: #333;
  2344. line-height: 1.4;
  2345. }
  2346. .metric-details-feedback .feedback-header mat-icon {
  2347. color: #6366f1;
  2348. font-size: 24px;
  2349. width: 24px;
  2350. height: 24px;
  2351. flex-shrink: 0;
  2352. display: inline-flex;
  2353. align-items: center;
  2354. justify-content: center;
  2355. }
  2356. .metric-details-feedback .metric-info {
  2357. display: flex;
  2358. flex-direction: column;
  2359. gap: 12px;
  2360. }
  2361. .metric-details-feedback .info-item {
  2362. display: flex;
  2363. justify-content: space-between;
  2364. align-items: center;
  2365. padding: 8px 0;
  2366. border-bottom: 1px solid #f0f0f0;
  2367. }
  2368. .metric-details-feedback .info-item:last-child {
  2369. border-bottom: none;
  2370. }
  2371. .metric-details-feedback .label {
  2372. color: #666;
  2373. font-weight: 500;
  2374. }
  2375. .metric-details-feedback .value {
  2376. font-weight: 600;
  2377. display: flex;
  2378. align-items: center;
  2379. gap: 8px;
  2380. line-height: 1.4;
  2381. }
  2382. .metric-details-feedback .value mat-icon {
  2383. font-size: 18px;
  2384. width: 18px;
  2385. height: 18px;
  2386. flex-shrink: 0;
  2387. display: inline-flex;
  2388. align-items: center;
  2389. justify-content: center;
  2390. }
  2391. .metric-details-feedback .value.excellent {
  2392. color: #10b981;
  2393. }
  2394. .metric-details-feedback .value.good {
  2395. color: #3b82f6;
  2396. }
  2397. .metric-details-feedback .value.warning {
  2398. color: #f59e0b;
  2399. }
  2400. .metric-details-feedback .value.poor {
  2401. color: #ef4444;
  2402. }
  2403. .metric-details-feedback .trend-positive {
  2404. color: #10b981;
  2405. }
  2406. .metric-details-feedback .trend-negative {
  2407. color: #ef4444;
  2408. }
  2409. .metric-details-feedback .trend-neutral {
  2410. color: #6b7280;
  2411. }
  2412. `;
  2413. document.head.appendChild(style);
  2414. const overlay = document.createElement('div');
  2415. overlay.style.cssText = `
  2416. position: fixed;
  2417. top: 0;
  2418. left: 0;
  2419. width: 100%;
  2420. height: 100%;
  2421. background: rgba(0,0,0,0.5);
  2422. z-index: 9999;
  2423. animation: fadeIn 0.3s ease-out;
  2424. `;
  2425. overlay.addEventListener('click', () => {
  2426. document.body.removeChild(overlay);
  2427. document.body.removeChild(feedback);
  2428. document.head.removeChild(style);
  2429. });
  2430. document.body.appendChild(overlay);
  2431. document.body.appendChild(feedback);
  2432. setTimeout(() => {
  2433. if (document.body.contains(overlay)) {
  2434. document.body.removeChild(overlay);
  2435. document.body.removeChild(feedback);
  2436. document.head.removeChild(style);
  2437. }
  2438. }, 5000);
  2439. }
  2440. private showMetricTrendFeedback(metric: PerformanceMetric) {
  2441. const feedback = document.createElement('div');
  2442. feedback.className = 'metric-trend-feedback';
  2443. feedback.innerHTML = `
  2444. <div class="feedback-header">
  2445. <mat-icon>trending_up</mat-icon>
  2446. <span>${metric.title}趋势分析</span>
  2447. </div>
  2448. <div class="feedback-content">
  2449. <div class="trend-chart-placeholder">
  2450. <mat-icon>show_chart</mat-icon>
  2451. <p>趋势图表正在加载...</p>
  2452. </div>
  2453. <div class="trend-summary">
  2454. <div class="summary-item">
  2455. <span class="label">当前趋势:</span>
  2456. <span class="value trend-${metric.trend.type}">
  2457. <mat-icon>${metric.trend.icon}</mat-icon>
  2458. ${metric.trend.type === 'positive' ? '上升' : metric.trend.type === 'negative' ? '下降' : '平稳'}
  2459. </span>
  2460. </div>
  2461. <div class="summary-item">
  2462. <span class="label">变化幅度:</span>
  2463. <span class="value">${metric.trend.value}</span>
  2464. </div>
  2465. <div class="summary-item">
  2466. <span class="label">对比周期:</span>
  2467. <span class="value">较上月</span>
  2468. </div>
  2469. </div>
  2470. </div>
  2471. `;
  2472. feedback.style.cssText = `
  2473. position: fixed;
  2474. top: 50%;
  2475. left: 50%;
  2476. transform: translate(-50%, -50%);
  2477. background: white;
  2478. border-radius: 12px;
  2479. box-shadow: 0 8px 32px rgba(0,0,0,0.15);
  2480. padding: 24px;
  2481. z-index: 10000;
  2482. min-width: 450px;
  2483. max-width: 550px;
  2484. animation: slideInScale 0.3s ease-out;
  2485. `;
  2486. const style = document.createElement('style');
  2487. style.textContent = `
  2488. .metric-trend-feedback .feedback-header {
  2489. display: flex;
  2490. align-items: center;
  2491. gap: 16px;
  2492. margin-bottom: 20px;
  2493. font-size: 18px;
  2494. font-weight: 600;
  2495. color: #333;
  2496. line-height: 1.4;
  2497. }
  2498. .metric-trend-feedback .feedback-header mat-icon {
  2499. color: #6366f1;
  2500. font-size: 24px;
  2501. width: 24px;
  2502. height: 24px;
  2503. flex-shrink: 0;
  2504. display: inline-flex;
  2505. align-items: center;
  2506. justify-content: center;
  2507. }
  2508. .metric-trend-feedback .trend-chart-placeholder {
  2509. background: #f8fafc;
  2510. border: 2px dashed #cbd5e1;
  2511. border-radius: 8px;
  2512. padding: 40px;
  2513. text-align: center;
  2514. margin-bottom: 20px;
  2515. }
  2516. .metric-trend-feedback .trend-chart-placeholder mat-icon {
  2517. font-size: 48px;
  2518. width: 48px;
  2519. height: 48px;
  2520. color: #94a3b8;
  2521. margin-bottom: 12px;
  2522. }
  2523. .metric-trend-feedback .trend-chart-placeholder p {
  2524. color: #64748b;
  2525. margin: 0;
  2526. font-size: 14px;
  2527. }
  2528. .metric-trend-feedback .trend-summary {
  2529. display: flex;
  2530. flex-direction: column;
  2531. gap: 12px;
  2532. }
  2533. .metric-trend-feedback .summary-item {
  2534. display: flex;
  2535. justify-content: space-between;
  2536. align-items: center;
  2537. padding: 8px 0;
  2538. border-bottom: 1px solid #f0f0f0;
  2539. }
  2540. .metric-trend-feedback .summary-item:last-child {
  2541. border-bottom: none;
  2542. }
  2543. .metric-trend-feedback .label {
  2544. color: #666;
  2545. font-weight: 500;
  2546. }
  2547. .metric-trend-feedback .value {
  2548. font-weight: 600;
  2549. display: flex;
  2550. align-items: center;
  2551. gap: 8px;
  2552. line-height: 1.4;
  2553. }
  2554. .metric-trend-feedback .value mat-icon {
  2555. font-size: 18px;
  2556. width: 18px;
  2557. height: 18px;
  2558. flex-shrink: 0;
  2559. display: inline-flex;
  2560. align-items: center;
  2561. justify-content: center;
  2562. }
  2563. .metric-trend-feedback .trend-positive {
  2564. color: #10b981;
  2565. }
  2566. .metric-trend-feedback .trend-negative {
  2567. color: #ef4444;
  2568. }
  2569. .metric-trend-feedback .trend-neutral {
  2570. color: #6b7280;
  2571. }
  2572. `;
  2573. document.head.appendChild(style);
  2574. const overlay = document.createElement('div');
  2575. overlay.style.cssText = `
  2576. position: fixed;
  2577. top: 0;
  2578. left: 0;
  2579. width: 100%;
  2580. height: 100%;
  2581. background: rgba(0,0,0,0.5);
  2582. z-index: 9999;
  2583. animation: fadeIn 0.3s ease-out;
  2584. `;
  2585. overlay.addEventListener('click', () => {
  2586. document.body.removeChild(overlay);
  2587. document.body.removeChild(feedback);
  2588. document.head.removeChild(style);
  2589. });
  2590. document.body.appendChild(overlay);
  2591. document.body.appendChild(feedback);
  2592. setTimeout(() => {
  2593. if (document.body.contains(overlay)) {
  2594. document.body.removeChild(overlay);
  2595. document.body.removeChild(feedback);
  2596. document.head.removeChild(style);
  2597. }
  2598. }, 5000);
  2599. }
  2600. private showMetricsRefreshFeedback() {
  2601. const feedback = document.createElement('div');
  2602. feedback.className = 'metrics-feedback';
  2603. feedback.innerHTML = `
  2604. <mat-icon>refresh</mat-icon>
  2605. <span>绩效指标已刷新</span>
  2606. `;
  2607. feedback.style.cssText = `
  2608. position: fixed;
  2609. top: 20px;
  2610. right: 20px;
  2611. background: #4CAF50;
  2612. color: white;
  2613. padding: 12px 20px;
  2614. border-radius: 8px;
  2615. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  2616. z-index: 1000;
  2617. display: flex;
  2618. align-items: center;
  2619. gap: 8px;
  2620. animation: slideInRight 0.3s ease-out;
  2621. `;
  2622. document.body.appendChild(feedback);
  2623. setTimeout(() => {
  2624. feedback.style.animation = 'slideOutRight 0.3s ease-in';
  2625. setTimeout(() => {
  2626. document.body.removeChild(feedback);
  2627. }, 300);
  2628. }, 2000);
  2629. }
  2630. private showExportFeedback() {
  2631. const feedback = document.createElement('div');
  2632. feedback.className = 'export-feedback';
  2633. feedback.innerHTML = `
  2634. <mat-icon>file_download</mat-icon>
  2635. <span>报告导出成功</span>
  2636. `;
  2637. feedback.style.cssText = `
  2638. position: fixed;
  2639. top: 20px;
  2640. right: 20px;
  2641. background: #2196F3;
  2642. color: white;
  2643. padding: 12px 20px;
  2644. border-radius: 8px;
  2645. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  2646. z-index: 1000;
  2647. display: flex;
  2648. align-items: center;
  2649. gap: 8px;
  2650. animation: slideInRight 0.3s ease-out;
  2651. `;
  2652. document.body.appendChild(feedback);
  2653. setTimeout(() => {
  2654. feedback.style.animation = 'slideOutRight 0.3s ease-in';
  2655. setTimeout(() => {
  2656. document.body.removeChild(feedback);
  2657. }, 300);
  2658. }, 2000);
  2659. }
  2660. // 绩效对比相关方法
  2661. onComparisonModeChange(event: any): void {
  2662. this.comparisonMode = event.value;
  2663. this.updateComparison();
  2664. }
  2665. updateComparison(): void {
  2666. // 更新显示的列
  2667. this.horizontalDisplayedColumns = ['name', ...this.selectedComparisonMetric, 'actions'];
  2668. // 根据对比维度更新数据
  2669. this.updateComparisonData();
  2670. }
  2671. private updateComparisonData(): void {
  2672. // 模拟根据不同维度更新数据
  2673. switch (this.selectedComparisonDimension) {
  2674. case 'department':
  2675. // 部门对比数据已经设置
  2676. break;
  2677. case 'period':
  2678. this.updatePeriodComparisonData();
  2679. break;
  2680. case 'individual':
  2681. this.updateIndividualComparisonData();
  2682. break;
  2683. case 'project':
  2684. this.updateProjectComparisonData();
  2685. break;
  2686. }
  2687. }
  2688. private updatePeriodComparisonData(): void {
  2689. this.horizontalComparisonData = [
  2690. {
  2691. id: 1,
  2692. name: '2024年Q1',
  2693. icon: 'calendar_today',
  2694. iconClass: 'period-icon',
  2695. completion: '88%',
  2696. quality: '85%',
  2697. efficiency: '82%',
  2698. satisfaction: '87%',
  2699. innovation: '80%'
  2700. },
  2701. {
  2702. id: 2,
  2703. name: '2024年Q2',
  2704. icon: 'calendar_today',
  2705. iconClass: 'period-icon',
  2706. completion: '90%',
  2707. quality: '88%',
  2708. efficiency: '85%',
  2709. satisfaction: '89%',
  2710. innovation: '85%'
  2711. },
  2712. {
  2713. id: 3,
  2714. name: '2024年Q3',
  2715. icon: 'calendar_today',
  2716. iconClass: 'period-icon',
  2717. completion: '92%',
  2718. quality: '90%',
  2719. efficiency: '88%',
  2720. satisfaction: '91%',
  2721. innovation: '88%'
  2722. }
  2723. ];
  2724. this.verticalComparisonData = this.horizontalComparisonData.map((item, index) => ({
  2725. ...item,
  2726. category: '季度数据',
  2727. overallScore: 85 + index * 3,
  2728. rank: index + 1
  2729. }));
  2730. }
  2731. private updateIndividualComparisonData(): void {
  2732. this.horizontalComparisonData = [
  2733. {
  2734. id: 1,
  2735. name: '张三',
  2736. icon: 'person',
  2737. iconClass: 'person-icon',
  2738. completion: '95%',
  2739. quality: '92%',
  2740. efficiency: '88%',
  2741. satisfaction: '94%',
  2742. innovation: '90%'
  2743. },
  2744. {
  2745. id: 2,
  2746. name: '李四',
  2747. icon: 'person',
  2748. iconClass: 'person-icon',
  2749. completion: '88%',
  2750. quality: '90%',
  2751. efficiency: '92%',
  2752. satisfaction: '87%',
  2753. innovation: '85%'
  2754. },
  2755. {
  2756. id: 3,
  2757. name: '王五',
  2758. icon: 'person',
  2759. iconClass: 'person-icon',
  2760. completion: '90%',
  2761. quality: '85%',
  2762. efficiency: '90%',
  2763. satisfaction: '88%',
  2764. innovation: '92%'
  2765. }
  2766. ];
  2767. this.verticalComparisonData = this.horizontalComparisonData.map((item, index) => ({
  2768. ...item,
  2769. category: '员工个人',
  2770. overallScore: 90 - index * 2,
  2771. rank: index + 1
  2772. }));
  2773. }
  2774. private updateProjectComparisonData(): void {
  2775. this.horizontalComparisonData = [
  2776. {
  2777. id: 1,
  2778. name: '项目Alpha',
  2779. icon: 'work',
  2780. iconClass: 'project-icon',
  2781. completion: '95%',
  2782. quality: '90%',
  2783. efficiency: '85%',
  2784. satisfaction: '92%',
  2785. innovation: '88%'
  2786. },
  2787. {
  2788. id: 2,
  2789. name: '项目Beta',
  2790. icon: 'work',
  2791. iconClass: 'project-icon',
  2792. completion: '88%',
  2793. quality: '88%',
  2794. efficiency: '90%',
  2795. satisfaction: '85%',
  2796. innovation: '90%'
  2797. },
  2798. {
  2799. id: 3,
  2800. name: '项目Gamma',
  2801. icon: 'work',
  2802. iconClass: 'project-icon',
  2803. completion: '92%',
  2804. quality: '85%',
  2805. efficiency: '88%',
  2806. satisfaction: '90%',
  2807. innovation: '85%'
  2808. }
  2809. ];
  2810. this.verticalComparisonData = this.horizontalComparisonData.map((item, index) => ({
  2811. ...item,
  2812. category: '项目数据',
  2813. overallScore: 88 + index,
  2814. rank: index + 1
  2815. }));
  2816. }
  2817. addComparisonItem(): void {
  2818. const dialogRef = this.dialog.open(AddComparisonDialogComponent, {
  2819. width: '700px',
  2820. panelClass: 'hr-dialog',
  2821. backdropClass: 'hr-dialog-backdrop',
  2822. data: {
  2823. dimension: this.selectedComparisonDimension,
  2824. availableMetrics: this.selectedComparisonMetric
  2825. }
  2826. });
  2827. dialogRef.afterClosed().subscribe((result: ComparisonItemData) => {
  2828. if (result) {
  2829. const newId = Math.max(...this.horizontalComparisonData.map(item => item.id)) + 1;
  2830. // 计算综合评分
  2831. const scores = Object.values(result.metrics).map(value =>
  2832. parseInt(value.replace('%', ''))
  2833. );
  2834. const overallScore = Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length);
  2835. const newItem = {
  2836. id: newId,
  2837. name: result.name,
  2838. icon: result.icon,
  2839. iconClass: result.iconClass,
  2840. ...result.metrics
  2841. };
  2842. this.horizontalComparisonData.push(newItem);
  2843. this.verticalComparisonData.push({
  2844. ...newItem,
  2845. category: result.category,
  2846. overallScore: overallScore,
  2847. rank: this.verticalComparisonData.length + 1
  2848. });
  2849. // 重新排序纵向对比数据
  2850. this.verticalComparisonData.sort((a, b) => b.overallScore - a.overallScore);
  2851. this.verticalComparisonData.forEach((item, index) => {
  2852. item.rank = index + 1;
  2853. });
  2854. this.showAddItemFeedback(result.name);
  2855. }
  2856. });
  2857. }
  2858. removeComparisonItem(id: number): void {
  2859. this.horizontalComparisonData = this.horizontalComparisonData.filter(item => item.id !== id);
  2860. this.verticalComparisonData = this.verticalComparisonData.filter(item => item.id !== id);
  2861. this.showRemoveItemFeedback();
  2862. }
  2863. viewComparisonDetails(id: number): void {
  2864. const item = this.horizontalComparisonData.find(item => item.id === id);
  2865. if (item) {
  2866. console.log('查看详情:', item);
  2867. // 这里可以打开详情弹窗或跳转到详情页面
  2868. }
  2869. }
  2870. getMetricDisplayName(metric: string): string {
  2871. const metricNames: { [key: string]: string } = {
  2872. completion: '完成率',
  2873. quality: '质量评分',
  2874. efficiency: '效率指数',
  2875. satisfaction: '满意度',
  2876. innovation: '创新度'
  2877. };
  2878. return metricNames[metric] || metric;
  2879. }
  2880. // 离职原因分析相关方法
  2881. updateResignationData(): void {
  2882. // 根据时间范围更新数据
  2883. console.log('更新离职数据,时间范围:', this.resignationTimeRange);
  2884. // 这里可以调用API获取对应时间范围的数据
  2885. }
  2886. toggleDepartmentFilter(deptId: string): void {
  2887. const dept = this.resignationDepartments.find(d => d.id === deptId);
  2888. if (dept) {
  2889. dept.selected = !dept.selected;
  2890. this.updateFilteredResignationData();
  2891. }
  2892. }
  2893. toggleLevelFilter(levelId: string): void {
  2894. const level = this.resignationLevels.find(l => l.id === levelId);
  2895. if (level) {
  2896. level.selected = !level.selected;
  2897. this.updateFilteredResignationData();
  2898. }
  2899. }
  2900. updateFilteredResignationData(): void {
  2901. // 根据筛选条件更新数据
  2902. const selectedDepts = this.resignationDepartments.filter(d => d.selected);
  2903. const selectedLevels = this.resignationLevels.filter(l => l.selected);
  2904. console.log('筛选条件更新:', { selectedDepts, selectedLevels });
  2905. // 这里可以重新计算统计数据和图表数据
  2906. }
  2907. exportResignationAnalysis(): void {
  2908. console.log('导出离职分析报告');
  2909. // 这里可以生成Excel或PDF报告
  2910. this.showExportFeedback();
  2911. }
  2912. viewReasonDetails(reasonId: string): void {
  2913. const reason = this.resignationReasons.find(r => r.id === reasonId);
  2914. if (reason) {
  2915. this.selectedReason = reason;
  2916. this.selectedDetailAnalysis = this.getDetailAnalysis(reasonId);
  2917. this.selectedImprovementPlan = this.getImprovementPlan(reasonId);
  2918. this.showDetailPanel = true;
  2919. }
  2920. }
  2921. viewImprovementPlan(reasonId: string): void {
  2922. const reason = this.resignationReasons.find(r => r.id === reasonId);
  2923. if (reason) {
  2924. this.selectedReason = reason;
  2925. this.selectedDetailAnalysis = this.getDetailAnalysis(reasonId);
  2926. this.selectedImprovementPlan = this.getImprovementPlan(reasonId);
  2927. this.showDetailPanel = true;
  2928. }
  2929. }
  2930. closeDetailPanel(): void {
  2931. this.showDetailPanel = false;
  2932. this.selectedReason = null;
  2933. this.selectedDetailAnalysis = null;
  2934. this.selectedImprovementPlan = null;
  2935. }
  2936. exportDetailReport(): void {
  2937. if (this.selectedReason) {
  2938. // 导出详细报告的逻辑
  2939. this.snackBar.open(`正在导出"${this.selectedReason.name}"的详细报告...`, '关闭', {
  2940. duration: 3000,
  2941. horizontalPosition: 'center',
  2942. verticalPosition: 'top'
  2943. });
  2944. }
  2945. }
  2946. private getDetailAnalysis(reasonId: string): DetailAnalysis {
  2947. // 根据不同的离职原因返回对应的详细分析数据
  2948. const analysisData: { [key: string]: DetailAnalysis } = {
  2949. 'salary': {
  2950. overview: '薪资待遇问题是当前最主要的离职原因,占比28.5%。主要体现在基本薪资偏低、绩效奖金不透明、福利待遇缺乏竞争力等方面。',
  2951. keyFactors: ['基本薪资偏低', '绩效考核不透明', '福利待遇单一', '薪资调整机制缺失', '市场竞争力不足'],
  2952. impactAnalysis: {
  2953. shortTerm: ['优秀员工流失加速', '招聘成本增加', '团队士气下降', '工作效率降低'],
  2954. longTerm: ['人才竞争力下降', '企业声誉受损', '核心技能流失', '业务发展受阻']
  2955. },
  2956. relatedDepartments: ['人力资源部', '财务部', '各业务部门'],
  2957. timeDistribution: [
  2958. { period: '第一季度', count: 3, percentage: 21.4 },
  2959. { period: '第二季度', count: 4, percentage: 28.6 },
  2960. { period: '第三季度', count: 5, percentage: 35.7 },
  2961. { period: '第四季度', count: 2, percentage: 14.3 }
  2962. ]
  2963. },
  2964. 'career': {
  2965. overview: '职业发展问题占比22.8%,主要反映在晋升通道不明确、技能培训不足、职业规划缺乏指导等方面。',
  2966. keyFactors: ['晋升通道狭窄', '培训机会有限', '职业规划缺失', '技能发展停滞', '内部流动性差'],
  2967. impactAnalysis: {
  2968. shortTerm: ['员工积极性下降', '学习动力不足', '创新能力减弱'],
  2969. longTerm: ['组织活力下降', '人才梯队断层', '竞争优势丧失']
  2970. },
  2971. relatedDepartments: ['人力资源部', '培训部', '各业务部门'],
  2972. timeDistribution: [
  2973. { period: '第一季度', count: 2, percentage: 18.2 },
  2974. { period: '第二季度', count: 3, percentage: 27.3 },
  2975. { period: '第三季度', count: 4, percentage: 36.4 },
  2976. { period: '第四季度', count: 2, percentage: 18.2 }
  2977. ]
  2978. },
  2979. 'workload': {
  2980. overview: '工作压力问题占比18.3%,主要表现为工作量过大、工作时间过长、工作节奏过快等。',
  2981. keyFactors: ['工作量过大', '加班频繁', '工作节奏快', '压力管理缺失', '工作生活平衡差'],
  2982. impactAnalysis: {
  2983. shortTerm: ['员工疲劳度增加', '工作质量下降', '健康问题增多'],
  2984. longTerm: ['员工流失率上升', '企业形象受损', '可持续发展受阻']
  2985. },
  2986. relatedDepartments: ['人力资源部', '运营部', '项目管理部'],
  2987. timeDistribution: [
  2988. { period: '第一季度', count: 2, percentage: 22.2 },
  2989. { period: '第二季度', count: 3, percentage: 33.3 },
  2990. { period: '第三季度', count: 2, percentage: 22.2 },
  2991. { period: '第四季度', count: 2, percentage: 22.2 }
  2992. ]
  2993. }
  2994. };
  2995. return analysisData[reasonId] || {
  2996. overview: '暂无详细分析数据',
  2997. keyFactors: [],
  2998. impactAnalysis: { shortTerm: [], longTerm: [] },
  2999. relatedDepartments: [],
  3000. timeDistribution: []
  3001. };
  3002. }
  3003. private getImprovementPlan(reasonId: string): ImprovementPlan {
  3004. // 根据不同的离职原因返回对应的改进计划
  3005. const improvementPlans: { [key: string]: ImprovementPlan } = {
  3006. 'salary': {
  3007. priority: 'high',
  3008. timeline: '3-6个月',
  3009. actions: [
  3010. {
  3011. title: '薪酬体系重构',
  3012. description: '建立科学的薪酬体系,包括基本薪资、绩效奖金、福利待遇等全面优化',
  3013. responsible: '人力资源部',
  3014. deadline: '2024-04-30',
  3015. status: 'in_progress'
  3016. },
  3017. {
  3018. title: '市场薪酬调研',
  3019. description: '定期进行市场薪酬调研,确保薪酬水平具有市场竞争力',
  3020. responsible: '人力资源部',
  3021. deadline: '2024-03-15',
  3022. status: 'pending'
  3023. },
  3024. {
  3025. title: '绩效考核优化',
  3026. description: '完善绩效考核体系,建立透明公正的绩效评估机制',
  3027. responsible: '人力资源部',
  3028. deadline: '2024-05-31',
  3029. status: 'pending'
  3030. }
  3031. ],
  3032. expectedOutcome: '通过薪酬体系优化,预计可降低因薪资问题导致的离职率15-20%,提升员工满意度和忠诚度。',
  3033. successMetrics: [
  3034. '离职率下降15-20%',
  3035. '员工满意度调查薪酬满意度提升至80%以上',
  3036. '关键岗位人才保留率提升至90%以上',
  3037. '新员工入职率提升10%'
  3038. ],
  3039. resources: {
  3040. budget: '200-300万元',
  3041. personnel: ['人力资源总监', '薪酬专员', '财务经理', '各部门主管'],
  3042. tools: ['薪酬管理系统', '绩效考核平台', '市场调研工具']
  3043. }
  3044. },
  3045. 'career': {
  3046. priority: 'high',
  3047. timeline: '6-12个月',
  3048. actions: [
  3049. {
  3050. title: '职业发展通道设计',
  3051. description: '建立清晰的职业发展通道,包括技术路线和管理路线',
  3052. responsible: '人力资源部',
  3053. deadline: '2024-06-30',
  3054. status: 'pending'
  3055. },
  3056. {
  3057. title: '培训体系建设',
  3058. description: '建立完善的培训体系,包括新员工培训、技能提升培训、领导力培训等',
  3059. responsible: '培训部',
  3060. deadline: '2024-08-31',
  3061. status: 'pending'
  3062. },
  3063. {
  3064. title: '导师制度建立',
  3065. description: '建立导师制度,为员工提供职业发展指导和支持',
  3066. responsible: '人力资源部',
  3067. deadline: '2024-05-31',
  3068. status: 'pending'
  3069. }
  3070. ],
  3071. expectedOutcome: '通过职业发展体系建设,预计可降低因职业发展问题导致的离职率20-25%,提升员工成长满意度。',
  3072. successMetrics: [
  3073. '员工职业发展满意度提升至85%以上',
  3074. '内部晋升比例提升至60%以上',
  3075. '培训参与率达到95%以上',
  3076. '关键人才保留率提升至95%以上'
  3077. ],
  3078. resources: {
  3079. budget: '150-200万元',
  3080. personnel: ['人力资源总监', '培训经理', '各部门主管', '资深员工导师'],
  3081. tools: ['学习管理系统', '职业发展平台', '在线培训工具']
  3082. }
  3083. },
  3084. 'workload': {
  3085. priority: 'medium',
  3086. timeline: '3-6个月',
  3087. actions: [
  3088. {
  3089. title: '工作量评估与优化',
  3090. description: '对各岗位工作量进行科学评估,合理分配工作任务',
  3091. responsible: '运营部',
  3092. deadline: '2024-04-30',
  3093. status: 'pending'
  3094. },
  3095. {
  3096. title: '工作流程优化',
  3097. description: '优化工作流程,提高工作效率,减少不必要的工作环节',
  3098. responsible: '项目管理部',
  3099. deadline: '2024-05-31',
  3100. status: 'pending'
  3101. },
  3102. {
  3103. title: '压力管理培训',
  3104. description: '开展压力管理培训,帮助员工更好地应对工作压力',
  3105. responsible: '人力资源部',
  3106. deadline: '2024-03-31',
  3107. status: 'pending'
  3108. }
  3109. ],
  3110. expectedOutcome: '通过工作压力管理优化,预计可降低因工作压力导致的离职率10-15%,提升员工工作满意度。',
  3111. successMetrics: [
  3112. '员工工作压力满意度提升至75%以上',
  3113. '平均加班时间减少20%',
  3114. '员工健康指标改善',
  3115. '工作效率提升15%'
  3116. ],
  3117. resources: {
  3118. budget: '50-100万元',
  3119. personnel: ['运营总监', '项目经理', '人力资源专员', '心理咨询师'],
  3120. tools: ['工作量管理系统', '项目管理工具', '健康管理平台']
  3121. }
  3122. }
  3123. };
  3124. return improvementPlans[reasonId] || {
  3125. priority: 'medium',
  3126. timeline: '待定',
  3127. actions: [],
  3128. expectedOutcome: '暂无改进计划',
  3129. successMetrics: [],
  3130. resources: { budget: '待评估', personnel: [], tools: [] }
  3131. };
  3132. }
  3133. getMetricClass(value: string): string {
  3134. const numValue = parseInt(value);
  3135. if (numValue >= 90) return 'metric-excellent';
  3136. if (numValue >= 80) return 'metric-good';
  3137. if (numValue >= 70) return 'metric-average';
  3138. return 'metric-poor';
  3139. }
  3140. getMetricPercentage(value: string, metric: string): number {
  3141. return parseInt(value);
  3142. }
  3143. getProgressBarClass(value: string): string {
  3144. const numValue = parseInt(value);
  3145. if (numValue >= 90) return 'progress-excellent';
  3146. if (numValue >= 80) return 'progress-good';
  3147. if (numValue >= 70) return 'progress-average';
  3148. return 'progress-poor';
  3149. }
  3150. getOverallScoreClass(score: number): string {
  3151. if (score >= 90) return 'score-excellent';
  3152. if (score >= 80) return 'score-good';
  3153. if (score >= 70) return 'score-average';
  3154. return 'score-poor';
  3155. }
  3156. private showAddItemFeedback(itemName: string): void {
  3157. const feedback = document.createElement('div');
  3158. feedback.className = 'add-item-feedback';
  3159. feedback.innerHTML = `
  3160. <div class="feedback-content">
  3161. <mat-icon>add_circle</mat-icon>
  3162. <span>对比项"${itemName}"添加成功!</span>
  3163. </div>
  3164. `;
  3165. feedback.style.cssText = `
  3166. position: fixed;
  3167. top: 20px;
  3168. right: 20px;
  3169. background: linear-gradient(135deg, #2196F3, #1976D2);
  3170. color: white;
  3171. padding: 16px 24px;
  3172. border-radius: 12px;
  3173. box-shadow: 0 8px 32px rgba(33, 150, 243, 0.3);
  3174. z-index: 1000;
  3175. animation: slideInRight 0.3s ease-out;
  3176. `;
  3177. document.body.appendChild(feedback);
  3178. setTimeout(() => {
  3179. feedback.style.animation = 'slideOutRight 0.3s ease-in';
  3180. setTimeout(() => {
  3181. document.body.removeChild(feedback);
  3182. }, 300);
  3183. }, 2000);
  3184. }
  3185. private showRemoveItemFeedback(): void {
  3186. const feedback = document.createElement('div');
  3187. feedback.className = 'remove-item-feedback';
  3188. feedback.innerHTML = `
  3189. <div class="feedback-content">
  3190. <mat-icon>remove_circle</mat-icon>
  3191. <span>对比项删除成功!</span>
  3192. </div>
  3193. `;
  3194. feedback.style.cssText = `
  3195. position: fixed;
  3196. top: 20px;
  3197. right: 20px;
  3198. background: linear-gradient(135deg, #FF5722, #D84315);
  3199. color: white;
  3200. padding: 16px 24px;
  3201. border-radius: 12px;
  3202. box-shadow: 0 8px 32px rgba(255, 87, 34, 0.3);
  3203. z-index: 1000;
  3204. animation: slideInRight 0.3s ease-out;
  3205. `;
  3206. document.body.appendChild(feedback);
  3207. setTimeout(() => {
  3208. feedback.style.animation = 'slideOutRight 0.3s ease-in';
  3209. setTimeout(() => {
  3210. document.body.removeChild(feedback);
  3211. }, 300);
  3212. }, 2000);
  3213. }
  3214. }