ResultAnalysis.vue 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136
  1. <template>
  2. <div class="record-right" v-loading="loading">
  3. <!-- 考核表详情 -->
  4. <div class="title-container" style="height: 30px; justify-content: space-between;">
  5. <div class="title flex-box-ce">
  6. 考核详情
  7. <div class="flex-box-ce" style="margin-left: 10px;">
  8. <el-cascader ref="cascader" v-model="headValue" :options="options" :props="props"
  9. :placeholder="placeholder" :no-data-text="noDataText"></el-cascader>
  10. </div>
  11. </div>
  12. <div>
  13. <!-- 正太规则 -->
  14. <EditScoreList :scoreList="scoreList" @confirm="onConfrim" />
  15. <el-button type="primary" @click="exportToExcel('mytable', '结果分析')">导出明细</el-button>
  16. </div>
  17. </div>
  18. <div class="line"></div>
  19. <div class="info-box">
  20. <div style="width: 100%; display: flex;">
  21. <div style="width: 50%; border-right: 1px solid #f1f1f1;">
  22. <div style="font-size: 16px; font-weight: 600;">等级分布</div>
  23. <div ref="echarts2" class="echarts"></div>
  24. </div>
  25. <div style="width: 50%; padding: 0 20px; box-sizing: border-box;">
  26. <div style="font-size: 16px; font-weight: 600;">正态分布</div>
  27. <!-- 考核人员分数列表 -->
  28. <div class="score-list">
  29. <div class="score-item heartBeat animated">
  30. <div class="score-item-title">总人数</div>
  31. <div class="score-item-num">{{ userTotal }}</div>
  32. <div class="score-item-percent">{{ userComplete }}人已完成</div>
  33. </div>
  34. <div v-for="item in scoreList" class="score-item heartBeat animated">
  35. <div class="score-item-title">{{ item.name }}</div>
  36. <div class="score-item-num">{{users.filter(user => user.scoreResult === item.name).length}}
  37. </div>
  38. </div>
  39. </div>
  40. </div>
  41. </div>
  42. <!-- 考核人员列表 -->
  43. <div class="title-container" style="margin: 10px 0;">
  44. <div class="title">考核人员列表</div>
  45. </div>
  46. <el-select v-model="deptIds" clearable multiple placeholder="请选择部门"
  47. style="width: 300px; margin-bottom: 10px;" @change="changeDeptName">
  48. <el-option v-for="item in deptList" :key="item.id" :label="item.name" :value="item.id"></el-option>
  49. </el-select>
  50. <el-table v-if="filterUsers && filterUsers.length > 0" :data="filterUsers" style="width: 100%; "
  51. :header-cell-style="{ background: '#f5f7fa' }" :max-height="600">
  52. <el-table-column prop="employeeName" label="员工">
  53. </el-table-column>
  54. <el-table-column prop="departments" label="部门" align="center" width="300">
  55. <template slot-scope="scope">
  56. <el-tooltip class="item" effect="dark" placement="top">
  57. <div slot="content" style="max-width: 300px; ">
  58. {{ scope.row.departments | formatDeptName }}
  59. </div>
  60. <div class="oneLine">
  61. {{ scope.row.departments | formatDeptName }}
  62. </div>
  63. </el-tooltip>
  64. </template>
  65. </el-table-column>
  66. <el-table-column prop="status" label="考核状态">
  67. <template slot-scope="scope">
  68. <el-tag v-if="scope.row.status" type="success">
  69. 已完成
  70. </el-tag>
  71. <el-tag v-else="scope.row.status" type="warning">
  72. 进行中
  73. </el-tag>
  74. </template>
  75. </el-table-column>
  76. <el-table-column prop="level" label="评分">
  77. <template slot-scope="scope">
  78. <el-tag v-if="scope.row.level === '未评分'" type="info">
  79. 未评分
  80. </el-tag>
  81. <el-tag v-else>
  82. {{ scope.row.score }}
  83. </el-tag>
  84. </template>
  85. </el-table-column>
  86. <el-table-column prop="level" label="评级">
  87. <template slot-scope="scope">
  88. <el-tag v-if="scope.row.level === '未评分'" type="info">
  89. 未评级
  90. </el-tag>
  91. <el-tag v-else>
  92. {{ scope.row.level }}
  93. </el-tag>
  94. </template>
  95. </el-table-column>
  96. <el-table-column prop="scoreResult" label="正态分布">
  97. <template slot-scope="scope">
  98. <el-tag>
  99. {{ scope.row.scoreResult }}
  100. </el-tag>
  101. </template>
  102. </el-table-column>
  103. <el-table-column label="操作">
  104. <template slot-scope="scope">
  105. <el-link type="primary"
  106. @click="getDetails(scope.row.reviewId, scope.row.employeeId)">查看详情</el-link>
  107. </template>
  108. </el-table-column>
  109. </el-table>
  110. <!-- 指标列表 -->
  111. <div class="title-container" style="margin: 10px 0;">
  112. <div class="title">指标信息</div>
  113. </div>
  114. <el-tabs type="card" v-model="activeName" @tab-click="handleClick">
  115. <el-tab-pane label="默认" name="1">
  116. <el-table id="mytable" :data="filterTableData1" stripe style="width: 100%" border
  117. :header-cell-style="{ background: '#f5f7fa' }" :max-height="600">
  118. <el-table-column prop="employeeName" label="员工" align="center"></el-table-column>
  119. <el-table-column prop="departments" label="部门" align="center" width="300">
  120. <template slot-scope="scope">
  121. <el-tooltip class="item" effect="dark" placement="top">
  122. <div slot="content" style="max-width: 300px; ">
  123. {{ scope.row.departments | formatDeptName }}
  124. </div>
  125. <div class="oneLine">
  126. {{ scope.row.departments | formatDeptName }}
  127. </div>
  128. </el-tooltip>
  129. </template>
  130. </el-table-column>
  131. <el-table-column prop="title" label="指标" align="center">
  132. <template slot-scope="scope">
  133. <el-tooltip class="item" effect="dark" placement="top">
  134. <div v-html="scope.row.title" slot="content" style="max-width:300px"></div>
  135. <div class="oneLine">{{ scope.row.title }}</div>
  136. </el-tooltip>
  137. </template>
  138. </el-table-column>
  139. <el-table-column prop="target" label="目标" align="center">
  140. <template slot-scope="scope">
  141. <span v-if="scope.row.target !== null">
  142. {{ `${scope.row.target} ${scope.row.unit ? scope.row.unit : ''}` }}
  143. </span>
  144. <span v-else>--</span>
  145. </template>
  146. </el-table-column>
  147. <el-table-column prop="result" label="实际值" align="center">
  148. <template slot-scope="scope">
  149. <div>
  150. <span v-if="scope.row.result !== null">
  151. {{ `${scope.row.result} ${scope.row.unit ? scope.row.unit : ''}` }}
  152. </span>
  153. <span v-else>--</span>
  154. </div>
  155. </template>
  156. </el-table-column>
  157. <el-table-column prop="different" label="差距" align="center">
  158. <template slot-scope="scope">
  159. <el-tag v-if="scope.row.difference > 0" type="success">
  160. {{ scope.row.difference }}
  161. </el-tag>
  162. <el-tag v-else-if="scope.row.difference < 0" type="danger">
  163. {{ scope.row.difference }}
  164. </el-tag>
  165. <div v-else>
  166. --
  167. </div>
  168. </template>
  169. </el-table-column>
  170. <el-table-column prop="score" label="评分" align="center">
  171. <template slot-scope="scope">
  172. <div>
  173. {{ scope.row.score || '--' }}
  174. </div>
  175. </template>
  176. </el-table-column>
  177. <el-table-column prop="okrs" label="过程跟踪" align="center">
  178. <template slot-scope="scope">
  179. <el-link v-if="scope.row.okrs && scope.row.okrs.length > 0" type="primary"
  180. @click="openTargetList(scope.row.okrs)">
  181. 指标OKR
  182. </el-link>
  183. <span v-else class="fontColorC">暂无关联</span>
  184. </template>
  185. </el-table-column>
  186. </el-table>
  187. <div style="height: 100px;"></div>
  188. </el-tab-pane>
  189. <el-tab-pane label="按指标/目标/单位聚合" name="2">
  190. <el-table :data="filterTableData2" stripe style="width: 100%" border
  191. :header-cell-style="{ background: '#f5f7fa' }" :max-height="600">
  192. <el-table-column prop="departments" label="部门" align="center" width="300">
  193. <template slot-scope="scope">
  194. <el-tooltip class="item" effect="dark" placement="top">
  195. <div slot="content" style="max-width: 300px; ">
  196. {{ scope.row.departments | formatDeptName }}
  197. </div>
  198. <div class="oneLine">
  199. {{ scope.row.departments | formatDeptName }}
  200. </div>
  201. </el-tooltip>
  202. </template>
  203. </el-table-column>
  204. <el-table-column prop="title" label="指标" align="center">
  205. <template slot-scope="scope">
  206. <el-tooltip class="item" effect="dark" placement="top">
  207. <div v-html="scope.row.title" slot="content" style="max-width:300px"></div>
  208. <div class="oneLine">{{ scope.row.title }}</div>
  209. </el-tooltip>
  210. </template>
  211. </el-table-column>
  212. <el-table-column prop="target" label="目标" align="center">
  213. <template slot-scope="scope">
  214. <div>
  215. <span v-if="scope.row.target !== null">
  216. {{ `${scope.row.target} ${scope.row.unit ? scope.row.unit : ''}` }}
  217. </span>
  218. <span v-else>--</span>
  219. </div>
  220. </template>
  221. </el-table-column>
  222. <el-table-column prop="avgResult" label="均值" align="center">
  223. </el-table-column>
  224. <el-table-column prop="standardResultRate" label="超出目标比例" align="center">
  225. <template slot-scope="scope">
  226. <el-tag v-if="parseInt(scope.row.standardResultRate) > 0" type="success">
  227. {{ scope.row.standardResultRate }}
  228. </el-tag>
  229. <el-tag v-else-if="parseInt(scope.row.standardResultRate) < 0" type="danger">
  230. {{ scope.row.standardResultRate }}
  231. </el-tag>
  232. <el-tag v-else type="info">
  233. {{ scope.row.standardResultRate }}
  234. </el-tag>
  235. </template>
  236. </el-table-column>
  237. <el-table-column prop="standardCount" label="达标数" align="center" />
  238. <el-table-column prop="standardRate" label="达标率" align="center">
  239. <template slot-scope="scope">
  240. <el-tag v-if="parseInt(scope.row.standardRate) > 0" type="success">
  241. {{ scope.row.standardRate }}
  242. </el-tag>
  243. <el-tag v-else-if="parseInt(scope.row.standardRate) < 0" type="danger">
  244. {{ scope.row.standardRate }}
  245. </el-tag>
  246. <el-tag v-else type="info">
  247. {{ scope.row.standardRate }}
  248. </el-tag>
  249. </template>
  250. </el-table-column>
  251. <el-table-column prop="avgScore" label="平均分" align="center" />
  252. </el-table>
  253. <div style="height: 100px;"></div>
  254. </el-tab-pane>
  255. </el-tabs>
  256. </div>
  257. <!-- 关联okr -->
  258. <TargetListComp v-if="targetDialogVisible" :dialogVisible="targetDialogVisible" :ids="okrs"
  259. @close="closeTargetList">
  260. </TargetListComp>
  261. <!-- 员工绩效详情 -->
  262. <el-dialog title="员工绩效详情" :visible.sync="detailDialogVisible" @close="handleClose" :close-on-click-modal="false"
  263. :close-on-press-escape="true" center fullscreen :show-close="false">
  264. <div style=" width: 100%; height: 100%; position: relative;">
  265. <el-button round style="position: absolute; top: -65px; left: 0px; z-index: 99;"
  266. @click="detailDialogVisible = false">返回</el-button>
  267. <!-- 我的考核 -->
  268. <MyPerformance v-if="detailDialogVisible" :reviewId="reviewId" :sendEmployeeId='sendEmployeeId' />
  269. </div>
  270. </el-dialog>
  271. </div>
  272. </template>
  273. <script>
  274. let that;
  275. import { mapGetters } from 'vuex';
  276. import moment from 'moment';
  277. import _ from "lodash"
  278. import ECharts from 'echarts';
  279. import EditScoreList from './ExamineRecord/RightEamineComp/EditScoreList'
  280. import TargetListComp from "@/performance/views/assessManagement/TargetListComp.vue"; // 关联OKR弹框
  281. import MyPerformance from './MyPerformance'; // 我的考核
  282. import cloneDeep from 'lodash.clonedeep';
  283. import FileSaver from "file-saver";
  284. import XLSX from "xlsx";
  285. export default {
  286. components: {
  287. EditScoreList,
  288. TargetListComp,
  289. MyPerformance
  290. },
  291. data() {
  292. return {
  293. reviewPackageId: 0,
  294. detailInfo: null,
  295. year: '2025',
  296. headValue: [],
  297. options: [
  298. { value: '4', label: '月度', leaf: false, children: [] },
  299. { value: '3', label: '季度', leaf: false, children: [] },
  300. { value: '2', label: '半年度', leaf: false, children: [] },
  301. { value: '1', label: '年度', leaf: false, children: [] },
  302. { value: '0', label: '自定义', leaf: false, children: [] },
  303. ], //
  304. props: {
  305. value: 'value',
  306. label: 'label',
  307. children: 'children',
  308. lazy: true,
  309. lazyLoad(node, resolve) {
  310. that.getAssessTree(node, resolve);
  311. }
  312. },
  313. total: 0,
  314. loading: false,
  315. tableData: [],
  316. deptList: [], // 部门列表 - 树形结构
  317. dept_list: [], // 部门列表
  318. selected_dept_ids: [], // 选择的部门列表
  319. placeholder: "",
  320. chooseChildren: [],
  321. noDataText: '暂无数据',
  322. loading: false,
  323. activeName: "1",
  324. reviewPackageId: "",
  325. title: "默认标题",
  326. detailDialogVisible: false,
  327. reviewId: '',
  328. sendEmployeeId: "",
  329. startTime: "",
  330. endTime: "",
  331. deptIds: [],
  332. rank: "",
  333. rankList: [],
  334. deptList: [],
  335. scoreList: [],
  336. users: [], //考核人员列表
  337. filterUsers: [],
  338. tableData1: [], // 考核中的指标列表,
  339. tableData2: [], // 按单位/目标/聚合指标列表,
  340. filterTableData1: [],
  341. filterTableData2: [],
  342. distributionId: "",
  343. level_enable: false,
  344. packages: [],
  345. userTotal: 0,
  346. userComplete: 0,
  347. userIncomplete: 0,
  348. infos: [],
  349. gradeLevels: [],
  350. cateIds: [], // 选择的考核分类
  351. targetDialogVisible: false,
  352. okrs: [],
  353. };
  354. },
  355. watch: {
  356. detailInfo(v) {
  357. this.initData();
  358. },
  359. deptName(v) {},
  360. year(val) {
  361. this.getRecords()
  362. },
  363. headValue(val) {
  364. if (this.chooseChildren && this.chooseChildren.length > 0) {
  365. let obj = this.chooseChildren.find(child => child.value == this.headValue[1])
  366. if (obj) {
  367. let value = ''
  368. if (this.headValue[0] && this.headValue[0] == '0') value = "自定义 / "
  369. if (this.headValue[0] && this.headValue[0] == '1') value = "年度 / "
  370. if (this.headValue[0] && this.headValue[0] == '2') value = "半年度 / "
  371. if (this.headValue[0] && this.headValue[0] == '3') value = "季度 / "
  372. if (this.headValue[0] && this.headValue[0] == '4') value = "月度 / "
  373. this.placeholder = obj.label || ''
  374. }
  375. }
  376. this.getResultAnalyze()
  377. },
  378. },
  379. computed: {
  380. ...mapGetters(['user_info']),
  381. calcScoreList() {
  382. let scoreSet = new Set(this.users.map(item => Number(item.score)))
  383. let scores = [...scoreSet].sort((a, b) => b - a);
  384. let rate = 1;
  385. let scoreCount = scores.length; // 总人数
  386. let scoreIndex = 0;
  387. let scoreResult = {};
  388. this.scoreList.forEach(item => {
  389. let scale = item.scale / 100;
  390. let count = Math.round((scoreCount - scoreIndex) * scale / rate);
  391. rate -= scale;
  392. if (count <= 0) return;
  393. for (let i = scoreIndex; i < (scoreIndex + count); i++) {
  394. scoreResult[scores[i]] = item.name;
  395. }
  396. scoreIndex += count;
  397. });
  398. return this.users.map(item => {
  399. let result = { ...item };
  400. result.scoreResult = scoreResult[item.score] || '--'
  401. return result;
  402. }).sort((a, b) => b.score - a.score);
  403. }
  404. },
  405. filters: {
  406. formatDate(val) {
  407. if (val) return moment(val).format('YYYY-MM-DD')
  408. else return "--"
  409. },
  410. formatDeptName(val) {
  411. let str = '';
  412. if (val && val.length > 0) {
  413. val.forEach(dept => {
  414. str += dept.name + ","
  415. })
  416. str = str.substr(0, str.length - 1)
  417. } else {
  418. str = "--"
  419. }
  420. return str
  421. }
  422. },
  423. mounted() {
  424. if (this.detailInfo) this.initData();
  425. },
  426. created() {
  427. that = this;
  428. this.getRecords('4'); // 优先获取当月最新考核数据 递归周期类型,获取考核数据,优先按月,季,半年度,年度来调用
  429. },
  430. methods: {
  431. exportToExcel(tableId, fileName) {
  432. // exportToExcel(tableId, fileName)
  433. this.downloadLoading = true
  434. const xlsxParam = { raw: true };
  435. const wb = XLSX.utils.table_to_book(document.getElementById(tableId), xlsxParam);
  436. // 遍历工作表中的所有单元格,处理换行
  437. const ws = wb.Sheets[wb.SheetNames[0]]; // 工作簿1
  438. for (const cell in ws) {
  439. if (ws[cell].v && typeof ws[cell].v === 'string') {
  440. ws[cell].v = ws[cell].v.replace(/\n/g, '\n');
  441. ws[cell].s = {
  442. alignment: {
  443. wrapText: true // 启用自动换行
  444. }
  445. };
  446. }
  447. }
  448. const wbout = XLSX.write(wb, { bookType: 'xlsx', bookSST: true, type: 'array' });
  449. try {
  450. FileSaver.saveAs(new Blob([wbout], { type: 'application/octet-stream' }), fileName + '.xlsx');
  451. } catch (e) {
  452. console.error(e, wbout);
  453. }
  454. this.downloadLoading = false;
  455. },
  456. getAssessTree(node, resolve) {
  457. if (this.options.length == 0) {
  458. return
  459. }
  460. const { value } = node;
  461. this.chooseChildren = node.children // 用来回显选择的文本
  462. node.children = []
  463. let url = `/performance/statistics/cycle/info/${this.user_info.site_id}/${value}`
  464. this.$axiosUser("get", url, {}).then(res => {
  465. let { data: { data: { items, cycleType } } } = res
  466. if (items && items.length > 0) {
  467. items.forEach(item => {
  468. item.leaf = true;
  469. item.label = item.remark
  470. })
  471. resolve(items)
  472. } else {
  473. resolve([])
  474. }
  475. }).finally(() => {
  476. // this.tableDataLoad = false;
  477. });
  478. },
  479. getRecords(cycle) {
  480. if (cycle < 0) {
  481. cycle = 0
  482. return
  483. }
  484. this.loading = true
  485. // 周期种类 0-自定义 1-年度 2-半年度 3-季度 4-月度
  486. let url = `/performance/statistics/cycle/info/${this.user_info.site_id}/${cycle}`
  487. this.$axiosUser("get", url, {}).then(res => {
  488. this.loading = false;
  489. let { data: { data: { items, cycleType } } } = res
  490. if (items && items.length > 0) {
  491. items.forEach(item => {
  492. item.leaf = true
  493. item.label = item.remark
  494. })
  495. let index = parseInt(4 - cycle)
  496. this.options[index].children = items
  497. this.headValue = [cycle + '', this.options[index].children[[0]].value]
  498. // if (this.headValue[1]) this.placeholder = this.options[parseInt(cycle)].children[[0]].label || ''
  499. } else {
  500. this.getRecords(parseInt(cycle) - 1) // 递归周期类型,获取考核数据,优先按月,季,半年度,年度来调用
  501. }
  502. })
  503. },
  504. getResultAnalyze() {
  505. this.reviewPackageId = 0
  506. let url = `/performance/statistics/cycle/${this.user_info.site_id}/${this.headValue[0]}`
  507. if (!this.headValue[1]) return
  508. this.$axiosUser("get", url, { value: this.headValue[1] }).then(res => {
  509. this.detailInfo = res.data.data
  510. this.reviewPackageId = 1
  511. })
  512. },
  513. // 关闭绩效弹框回调事件
  514. handleClose() { },
  515. changeDeptName(v) {
  516. this.filterUsers = this.users.filter((item) => {
  517. const departmentMatch = this.deptIds.length === 0 || this.deptIds.some((depId) => item.departments.some(dep => dep.id == depId));
  518. return departmentMatch;
  519. });
  520. this.filterTableData1 = this.tableData1.filter((item) => {
  521. const departmentMatch = this.deptIds.length === 0 || this.deptIds.some((depId) => item.departments.some(dep => dep.id == depId));
  522. return departmentMatch;
  523. });
  524. this.filterTableData2 = this.tableData2.filter((item) => {
  525. const departmentMatch = this.deptIds.length === 0 || this.deptIds.some((depId) => item.departments.some(dep => dep.id == depId));
  526. return departmentMatch;
  527. });
  528. },
  529. async initData() {
  530. this.loading = true
  531. this.activeName = '1'
  532. this.deptIds = [];
  533. this.deptList = [];
  534. this.users = [];
  535. this.filterUsers = [];
  536. this.isShow = false
  537. this.tableData1 = [];
  538. this.tableData2 = [];
  539. this.filterTableData1 = [];
  540. this.filterTableData2 = [];
  541. let { indicators, startTime, endTime, distribution: { items }, users } = this.detailInfo
  542. await this.getAllSet();
  543. this.tableData1 = [];
  544. this.tableData2 = [];
  545. this.startTime = startTime;
  546. this.endTime = endTime;
  547. this.scoreList = items;
  548. this.distributionId = this.scoreList[0].id
  549. this.users = users;
  550. this.tableData1 = indicators;
  551. this.tableData1.forEach(item => {
  552. this.users.forEach(user => {
  553. if (user.employeeId == item.employeeId) {
  554. item.departments = user.departments
  555. }
  556. })
  557. if (item.target && item.result) {
  558. item.difference = item.result - item.target
  559. } else {
  560. item.difference = '--'
  561. }
  562. })
  563. this.filterTableData1 = this.tableData1
  564. this.userTotal = 0;
  565. this.userComplete = 0;
  566. this.userIncomplete = 0;
  567. let distribution = [];
  568. let userScores = []
  569. this.scoreList.forEach(item => {
  570. item.level = item.name;
  571. item.ratio = item.scale / 100
  572. distribution.push(item)
  573. })
  574. if (this.users && this.users.length > 0) {
  575. this.users.forEach(user => {
  576. this.userTotal++;
  577. this.userComplete += user.status === 1 ? 1 : 0;
  578. user.level = this.findGrade(user.score, this.gradeLevels);
  579. // this.rankList.push(user.level || '未评分')
  580. userScores.push(user.score)
  581. })
  582. this.infos = [
  583. { label: "总人数", num: this.userTotal },
  584. { label: "已完成", num: this.userComplete },
  585. { label: "未评分", num: this.userIncomplete },
  586. ]
  587. let scoreResult = this.assignLevels(userScores, distribution);
  588. this.users.forEach(item => {
  589. scoreResult.forEach(result => {
  590. if (result.scores.includes(item.score)) {
  591. item.scoreResult = result.level
  592. }
  593. })
  594. })
  595. this.isShow = true
  596. this.filterUsers = cloneDeep(this.users)
  597. this.initDeptList();
  598. this.getResult();
  599. }
  600. this.loading = false
  601. },
  602. initDeptList() {
  603. if (this.users && this.users.length > 0) {
  604. this.users.forEach(user => {
  605. if (user.departments && user.departments.length > 0) {
  606. user.departments.forEach(dep => {
  607. this.deptList.push({ id: dep.id, name: dep.name })
  608. })
  609. }
  610. })
  611. this.deptList = Array.from(new Set(this.deptList.map(JSON.stringify))).map(JSON.parse);
  612. }
  613. },
  614. getResult() {
  615. let xData = [], yData = []
  616. this.gradeLevels.forEach(item => {
  617. xData.push(item.name)
  618. yData.push(this.users.filter(user => user.level === item.name).length)
  619. })
  620. let option = {
  621. tooltip: {
  622. trigger: 'axis',
  623. axisPointer: {
  624. // 坐标轴指示器,坐标轴触发有效
  625. type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
  626. }
  627. },
  628. xAxis: {
  629. type: 'category',
  630. data: xData
  631. },
  632. yAxis: {
  633. type: 'value',
  634. minInterval: 1
  635. },
  636. series: [
  637. {
  638. data: yData,
  639. type: 'bar',
  640. itemStyle: {
  641. normal: {
  642. //这里是重点
  643. color: function (params) {
  644. //注意,如果颜色太少的话,后面颜色不会自动循环,最好多定义几个颜色
  645. var colorList = ['#409EFF', '#4ECB73', '#36CBCB', '#F2637B', '#FBD437', '#749f83', '#ca8622'];
  646. var index;
  647. //给大于颜色数量的柱体添加循环颜色的判断
  648. if (params.dataIndex >= colorList.length) {
  649. index = params.dataIndex - colorList.length;
  650. return colorList[index];
  651. }
  652. return colorList[params.dataIndex];
  653. }
  654. }
  655. },
  656. barMaxWidth: 30
  657. }
  658. ]
  659. };
  660. var myChart = ECharts.init(this.$refs.echarts2);
  661. myChart.setOption(option);
  662. window.onresize = myChart.resize;
  663. },
  664. getDetails(reviewId, employeeId) {
  665. this.reviewId = reviewId
  666. this.sendEmployeeId = employeeId
  667. this.detailDialogVisible = true
  668. // this.$bus.$emit('changeCurrentId', { currentId: '2', reviewId, employeeId })
  669. },
  670. changeCateIds(cateIds) {
  671. this.cateIds = cateIds
  672. this.$bus.$emit("finishEdit", this.reviewPackageId)
  673. },
  674. changeTitle(title) {
  675. this.title = title;
  676. this.$bus.$emit("finishEdit", this.reviewPackageId)
  677. },
  678. changeDate(data) {
  679. let { startTime, endTime } = data
  680. this.startTime = startTime
  681. this.endTime = endTime
  682. this.$bus.$emit("finishEdit", this.reviewPackageId)
  683. },
  684. // 获取全局等级设置
  685. async getAllSet() {
  686. let res = await this.$axiosUser('get', 'api/pro/per/user/base_config')
  687. let data = res.data.data;
  688. let levels = data.level_scope.levels;
  689. let gradeLevels = [];
  690. let max = 0;//最大值
  691. if (levels && levels.length > 0) {
  692. levels.forEach((item, index) => {
  693. var obj;
  694. if (index == 0) {
  695. obj = { name: item.name, max: Number(item.value), min: 0 };
  696. } else {
  697. obj = { name: item.name, max: Number(item.value), min: max };//当不是第一个等级时,最小值为上一个的最大值
  698. }
  699. max = item.value;
  700. gradeLevels.push(obj);
  701. })
  702. this.gradeLevels = gradeLevels
  703. }
  704. },
  705. // 查找分数对应的等级
  706. findGrade(score, gradeLevels) {
  707. for (let i = 0; i < gradeLevels.length; i++) {
  708. if (score && score >= gradeLevels[i].min && score && score <= gradeLevels[i].max) {
  709. return gradeLevels[i].name; // 返回对应的等级描述
  710. } else if (score && score <= gradeLevels[0].min) {
  711. return gradeLevels[0].name; // 返回对应的等级描述
  712. } else if (score && score >= gradeLevels[gradeLevels.length - 1].max) {
  713. return gradeLevels[gradeLevels.length - 1].name; // 返回对应的等级描述
  714. }
  715. }
  716. return "未评分"; // 如果分数不在任何范围内
  717. },
  718. assignLevels(scores, levelConfigs) {
  719. // 降序排序并去重(假设分数不重复,可省略去重)
  720. const sortedScores = [...scores].sort((a, b) => b - a);
  721. const total = sortedScores.length;
  722. if (total === 0) return [];
  723. // 归一化处理比例
  724. const totalRatio = levelConfigs.reduce((sum, cfg) => sum + cfg.ratio, 0);
  725. const normalized = levelConfigs.map(cfg => cfg.ratio / totalRatio);
  726. // 计算每个等级的初始人数
  727. let counts = normalized.map(ratio => Math.floor(total * ratio));
  728. let remainder = total - counts.reduce((sum, c) => sum + c, 0);
  729. // 分配剩余人数,按优先级顺序
  730. let idx = 0;
  731. while (remainder > 0 && idx < counts.length) {
  732. counts[idx]++;
  733. remainder--;
  734. idx++;
  735. }
  736. // 构建结果:按人数切割数组
  737. let start = 0;
  738. return counts.map((count, i) => {
  739. const end = start + count;
  740. const levelScores = sortedScores.slice(start, end);
  741. start = end;
  742. return {
  743. level: levelConfigs[i].level,
  744. scores: levelScores
  745. };
  746. });
  747. },
  748. onConfrim(items) {
  749. if (!(items && items.length > 0)) return
  750. this.userTotal = 0;
  751. this.userComplete = 0;
  752. this.userIncomplete = 0;
  753. let distribution = [];
  754. let userScores = []
  755. this.filterUsers = []
  756. this.loading = true
  757. this.scoreList = items
  758. this.scoreList.forEach(item => {
  759. item.level = item.name;
  760. item.ratio = item.scale / 100
  761. distribution.push(item)
  762. })
  763. if (this.users && this.users.length > 0) {
  764. this.users.forEach(user => {
  765. this.userTotal++;
  766. this.userComplete += user.status === 1 ? 1 : 0;
  767. user.level = this.findGrade(user.score, this.gradeLevels);
  768. // this.rankList.push(user.level || '未评分')
  769. userScores.push(user.score)
  770. })
  771. this.infos = [
  772. { label: "总人数", num: this.userTotal },
  773. { label: "已完成", num: this.userComplete },
  774. { label: "未评分", num: this.userIncomplete },
  775. ]
  776. let scoreResult = this.assignLevels(userScores, distribution);
  777. this.users.forEach(item => {
  778. scoreResult.forEach(result => {
  779. if (result.scores.includes(item.score)) {
  780. item.scoreResult = result.level
  781. }
  782. })
  783. })
  784. this.filterUsers = cloneDeep(this.users)
  785. this.loading = false
  786. console.log(this.filterUsers);
  787. }
  788. },
  789. // 选项卡点击事件
  790. handleClick(tab, event) {
  791. this.tableData2 = []
  792. if (this.activeName == 2) {
  793. let groups = _.groupBy(this.tableData1, item => `${item.title}(_)${item.target === null || item.target === '' ? 'null' : item.target}(_)${item.unit === null || item.unit === '' ? 'null' : item.unit}`);
  794. Object.keys(groups).forEach(key => {
  795. let group = {
  796. title: '',
  797. target: '',
  798. unit: '',
  799. departments: [],
  800. userCount: 0,
  801. scoredCount: 0,
  802. standardCount: 0,
  803. failCount: 0,
  804. standardRate: '--',
  805. totalScore: 0,
  806. totalResult: 0,
  807. avgScore: 0,
  808. avgResult: 0,
  809. standardResultRate: '--'
  810. };
  811. groups[key].forEach(indicator => {
  812. group.title = indicator.title; // 指标名称
  813. group.target = indicator.target; // 目标
  814. group.unit = indicator.unit; // 单位
  815. group.departments = indicator.departments; // 单位
  816. let standardCount = indicator.difference !== '--' && indicator.difference >= 0 ? 1 : 0; //
  817. group.userCount += 1;
  818. group.scoredCount += indicator.score !== null ? 1 : 0;
  819. group.standardCount += standardCount;
  820. group.failCount += standardCount === 1 ? 0 : 1;
  821. if (indicator.score !== null) group.totalScore += indicator.score;
  822. if (indicator.result !== null) group.totalResult += indicator.result;
  823. });
  824. group.standardCount = group.standardCount;
  825. if (group.userCount > 0) {
  826. let rate = Math.floor(group.standardCount / group.userCount * 100 * 0.01);
  827. let avgScore = Math.floor(group.totalScore / group.userCount * 100 * 0.01);
  828. let avgResult = Math.floor(group.totalResult / group.userCount * 100 * 0.01);
  829. group.standardRate = rate > 0 ? `${rate}%` : '--';
  830. group.avgScore = avgScore !== 0 ? avgScore : '--';
  831. group.avgResult = avgResult !== 0 ? avgResult : '--';
  832. if (group.target !== null && group.avgResult !== '--') {
  833. let standardResultRate = Math.floor((group.avgResult - group.target) / group.target * 100 * 0.01 * 100);
  834. group.standardResultRate = standardResultRate !== 0 ? `${standardResultRate}%` : '--';
  835. }
  836. }
  837. this.tableData2.push(group);
  838. this.filterTableData2 = this.tableData2
  839. })
  840. }
  841. },
  842. closeTargetList() {
  843. this.targetDialogVisible = false
  844. },
  845. openTargetList(okrs) {
  846. if (okrs && okrs.length > 0) {
  847. this.okrs = okrs
  848. this.targetDialogVisible = true
  849. } else {
  850. return this.$message.error("暂无关联okr")
  851. }
  852. }
  853. }
  854. }
  855. </script>
  856. <style>
  857. .oneLine {
  858. overflow: hidden;
  859. white-space: nowrap;
  860. text-overflow: ellipsis;
  861. }
  862. </style>
  863. <style scoped="scoped" lang="scss">
  864. .record-right {
  865. width: 100%;
  866. border-radius: 5px;
  867. background: #fff;
  868. padding: 10px 20px;
  869. display: flex;
  870. flex-direction: column;
  871. overflow: hidden;
  872. .info-box {
  873. width: 100%;
  874. flex: 1;
  875. overflow-y: auto;
  876. /* 设置滚动条的宽度和背景色 */
  877. &::-webkit-scrollbar {
  878. width: 8px;
  879. height: 8px;
  880. background-color: #f9f9f9;
  881. }
  882. /* 设置滚动条滑块的样式 */
  883. &::-webkit-scrollbar-thumb {
  884. border-radius: 6px;
  885. background-color: #c1c1c1;
  886. }
  887. /* 设置滚动条滑块hover样式 */
  888. &::-webkit-scrollbar-thumb:hover {
  889. background-color: #a8a8a8;
  890. }
  891. /* 设置滚动条轨道的样式 */
  892. &::-webkit-scrollbar-track {
  893. box-shadow: inset 0 0 5px rgba(87, 175, 187, 0.1);
  894. border-radius: 6px;
  895. background: #ededed;
  896. }
  897. }
  898. .echarts {
  899. height: 350px;
  900. width: 100%;
  901. }
  902. .title-container {
  903. display: flex;
  904. align-items: center;
  905. .searchBox {
  906. width: 300px;
  907. .search-title {
  908. border-bottom: 1px solid #f1f1f1;
  909. font-size: 16px;
  910. font-weight: 700;
  911. padding: 0 10px;
  912. padding-bottom: 10px;
  913. }
  914. }
  915. .title {
  916. font-weight: 700;
  917. font-size: 16px;
  918. }
  919. }
  920. .line {
  921. width: 100%;
  922. height: 1px;
  923. background: #f1f1f1;
  924. margin: 10px 0;
  925. }
  926. .score-list {
  927. display: flex;
  928. flex-wrap: wrap;
  929. width: 100%;
  930. margin-top: 20px;
  931. .score-item {
  932. flex: 0 0 calc((100% - 100px) / 4);
  933. height: 100px;
  934. padding: 10px;
  935. margin: 0 20px 20px 0;
  936. box-sizing: border-box;
  937. border-radius: 6px;
  938. display: flex;
  939. flex-direction: column;
  940. align-items: center;
  941. justify-content: space-around;
  942. font-size: 16px;
  943. color: #999;
  944. box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
  945. &-title {
  946. font-weight: 600;
  947. color: #409EFF;
  948. }
  949. &-num {
  950. color: #000;
  951. font-weight: 600;
  952. }
  953. &:nth-child(5n) {
  954. /* 去除第5n个的margin-right */
  955. margin-right: 0;
  956. }
  957. }
  958. }
  959. /* 设置滚动条的宽度和背景色 */
  960. ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
  961. width: 6px;
  962. height: 6px;
  963. background-color: #f9f9f9;
  964. }
  965. /* 设置滚动条滑块的样式 */
  966. ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb {
  967. border-radius: 6px;
  968. background-color: #c1c1c1;
  969. }
  970. /* 设置滚动条滑块hover样式 */
  971. ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
  972. background-color: #a8a8a8;
  973. }
  974. /* 设置滚动条轨道的样式 */
  975. ::v-deep .el-table__body-wrapper::-webkit-scrollbar-track {
  976. box-shadow: inset 0 0 5px rgba(87, 175, 187, 0.1);
  977. border-radius: 6px;
  978. background: #ededed;
  979. }
  980. .user-info {
  981. width: 100%;
  982. height: 200px;
  983. overflow: hidden;
  984. overflow-x: auto;
  985. /* 设置滚动条的宽度和背景色 */
  986. &::-webkit-scrollbar {
  987. width: 10px;
  988. height: 10px;
  989. background-color: #f9f9f9;
  990. }
  991. /* 设置滚动条滑块的样式 */
  992. &::-webkit-scrollbar-thumb {
  993. border-radius: 6px;
  994. background-color: #c1c1c1;
  995. }
  996. /* 设置滚动条滑块hover样式 */
  997. &::-webkit-scrollbar-thumb:hover {
  998. background-color: #a8a8a8;
  999. }
  1000. /* 设置滚动条轨道的样式 */
  1001. &::-webkit-scrollbar-track {
  1002. box-shadow: inset 0 0 5px rgba(87, 175, 187, 0.1);
  1003. border-radius: 6px;
  1004. background: #ededed;
  1005. }
  1006. .info-card {
  1007. width: 220px;
  1008. height: 100%;
  1009. margin-right: 20px;
  1010. border: 1px solid #f1f1f1;
  1011. padding: 5px 10px;
  1012. border-radius: 6px;
  1013. box-sizing: border-box;
  1014. box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
  1015. .info {
  1016. display: flex;
  1017. align-items: center;
  1018. height: 36px;
  1019. .info-label {
  1020. width: 100px;
  1021. }
  1022. }
  1023. }
  1024. }
  1025. }
  1026. </style>