resultAnalysis.vue 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  1. <template>
  2. <div style="height: 100%;" :class="{ bg_fff: skeletonLoad }">
  3. <van-nav-bar title="结果分析" left-text="返回" left-arrow @click-left="$route_back" />
  4. <VanSkeleton :skeLoad="skeletonLoad" v-if="isYou">
  5. <template>
  6. <header>
  7. <div style="border-bottom: 1px solid #f1f1f1;" class="selector flex-box-ce flex-center-center" @click="openPanel()">
  8. <icon name="YMPicker_item_icon" style="margin-right: 6px;width: 0.3rem;height: 0.3rem;" class="fontColorC"></icon>
  9. <div>{{ paramsText }}</div>
  10. <van-icon name="arrow-down" />
  11. </div>
  12. </header>
  13. <scroller ref="me_scroller" class="all">
  14. <div style="padding-bottom: 1.5rem;">
  15. <div class="statementFoot">
  16. <van-tabs v-model="active" @change="statementTabs">
  17. <van-tab v-for="(item, index) in statementVanTabs" :key="index" :title="item.name"></van-tab>
  18. </van-tabs>
  19. <!-- 等级分布 -->
  20. <div class="statmentAnalysePie" v-show="active == 0">
  21. <div style="width: 7.5rem; height: 5rem;" id="statmentAnalysePie" ref="statmentAnalysePie"></div>
  22. <div class="PiePropNum">
  23. <div>总人数</div>
  24. <div>{{ userTotal }}人</div>
  25. </div>
  26. </div>
  27. <!-- 正态分布 -->
  28. <div v-show="active == 1">
  29. <div class="statementHnum">
  30. <div class="Hnumvfor">
  31. <span class="smHnLtit">总人数</span>
  32. <br />
  33. <span class="smHnLnum">{{ userTotal }}</span>
  34. <br />
  35. <span{{ userComplete }}人已完成</span>
  36. </div>
  37. <div v-for="(item, index) in scoreList" class="Hnumvfor" :key="index">
  38. <span class="smHnLtit">{{ item.name }}</span>
  39. <br />
  40. <span class="smHnLnum">{{users.filter(user => user.scoreResult === item.name).length}}</span>
  41. <br />
  42. </div>
  43. </div>
  44. </div>
  45. <div>
  46. <div>
  47. <!-- <van-search placeholder="请输入姓名" v-model="keyword" @input="keyVal()" /> -->
  48. <div class="flex-box-ce" style="border-bottom: 0.02rem solid #f1f1f1;">
  49. <div class="selectItem flex-1 font-flex-word fontColorC" style="border-right: 0.02rem solid #f1f1f1;" @click="showDept = true">
  50. <span>{{ deptName }}</span>
  51. <van-icon name="arrow-down" size="12"/>
  52. </div>
  53. <div class="selectItem flex-1 font-flex-word fontColorC" @click="showStatus = true">
  54. <span>{{ statusName }}</span>
  55. <van-icon name="arrow-down" size="12"/>
  56. </div>
  57. </div>
  58. <ol v-if="filterUsers && filterUsers.length > 0">
  59. <li @click="openDetail(item)" v-for="(item, index) in filterUsers" :key="index" class="flex-ce-box flex-d-center statmentperson" :style="index != 0 ? 'border-top: 0.02rem solid #e6e6e6;' : ''">
  60. <div class="flex-1 flex-box" style="align-items: center; justify-content: space-between;">
  61. <!-- <div class="score-result">{{ item.scoreResult }}</div> -->
  62. <div class="flex-box-ce flex-1">
  63. <userImage :id="item.employeeId" :user_name="item.employeeName" fontSize="0.15" width="0.63rem" height="0.63rem" style="margin-top:.08rem;"></userImage>
  64. <div style="padding-left:.2rem;">
  65. <span style="font-size:.3rem;">{{ item.employeeName }}</span>
  66. <br />
  67. <span class="font-flex-word" style="width: 3.8rem;display:inline-block;" v-if="item.dept_list && item.dept_list.length > 0">
  68. <span style="font-size:.25rem;" v-for="(arr, att) in item.dept_list" :key="att">
  69. {{ arr.dept_name }}
  70. <span v-if="item.dept_list.length - att > 1">,</span>
  71. </span>
  72. </span>
  73. </div>
  74. </div>
  75. <div style="display: flex; align-items: center; flex-direction: column;">
  76. <div v-if="item.status == 0" class="review-status">进行中</div>
  77. <div v-if="item.status == 1" class="review-status" style="background-color: #FF9600;">已结束</div>
  78. <div style="display: flex; align-items: center; white-wrap: no-wrap; font-size:.26rem; text-align:center; overflow: hidden;">
  79. <span>{{ item.score == undefined || item.score == null || item.score == '' ? '-' : item.score }}</span>
  80. /
  81. <span v-if="active == 1" style="color:#ffad67;">{{ item.scoreResult || '-' }}</span>
  82. <span v-if="active == 0" style="color:#ffad67;">{{ item.level ? item.level : '--'}}</span>
  83. </div>
  84. </div>
  85. </div>
  86. </li>
  87. </ol>
  88. <noData v-else content="无考核记录"></noData>
  89. </div>
  90. </div>
  91. </div>
  92. <template>
  93. <div class="indicator-list-title">指标分析</div>
  94. <div class="indicator-list">
  95. <div class="indicator-item" v-for="(item, index) in tableData2" :key="index">
  96. <div class="indicator-item-info">
  97. <div class="indicator-info-label">指标名称</div>
  98. <div class="indicator-info-value">{{ item.title }}</div>
  99. </div>
  100. <div class="indicator-item-info">
  101. <div class="indicator-info-label">目标</div>
  102. <div class="indicator-info-value">{{ item.target }} {{ item.unit ? item.unit : '' }}</div>
  103. </div>
  104. <div class="indicator-item-info">
  105. <div class="indicator-info-label">均值</div>
  106. <div class="indicator-info-value">{{ item.avgResult }} {{ item.unit ? item.unit : '' }}</div>
  107. </div>
  108. <div class="indicator-item-info">
  109. <div class="indicator-info-label">超出目标比例</div>
  110. <div v-if="parseInt(item.standardResultRate) > 0" class="color-green">{{ item.standardResultRate }}</div>
  111. <div v-else-if="parseInt(item.standardResultRate) < 0" class="color-red">{{ item.standardResultRate }}</div>
  112. <div v-else class="color-gray">{{ item.standardResultRate }}</div>
  113. </div>
  114. <div class="indicator-item-info">
  115. <div class="indicator-info-label">达标数</div>
  116. <div class="indicator-info-value">{{ item.standardCount }}</div>
  117. </div>
  118. <div class="indicator-item-info">
  119. <div class="indicator-info-label">达标率</div>
  120. <div v-if="parseInt(item.standardRate) > 0" class="color-green" >{{ item.standardRate }}</div>
  121. <div v-else-if="parseInt(item.standardRate) < 0" class="color-red" >{{ item.standardRate }}</div>
  122. <div v-else class="color-gray" >{{ item.standardRate }}</div>
  123. </div>
  124. <div class="indicator-item-info">
  125. <div class="indicator-info-label">平均分</div>
  126. <div class="indicator-info-value">{{ item.avgScore }}</div>
  127. </div>
  128. </div>
  129. </div>
  130. </template>
  131. </div>
  132. </scroller>
  133. </template>
  134. </VanSkeleton>
  135. <noData content="无考核记录" v-else></noData>
  136. <!-- 周期选择 -->
  137. <van-action-sheet v-model="pullonThePanel" :closeable="false">
  138. <div class="content">
  139. <van-picker ref="van_picker" show-toolbar :columns="cycleOptions" @cancel="pullonThePanel = false" value-key="name" @confirm="onConfirm" confirm-button-text="完成" />
  140. </div>
  141. </van-action-sheet>
  142. <!-- 部门搜索 -->
  143. <van-dialog v-model="showDept" width="300" confirm-button-text="确定" @confirm="handleConfirm" @cancel="handleCancel" :show-confirm-button="true" closeOnClickOverlay>
  144. <div style="max-height: 8rem; overflow: auto;" class="scroll-bar">
  145. <div class="dept-list">
  146. <div class="checkbox-wrapper" v-for="(item, index) in department_list" :key="index">
  147. <input type="checkbox" :id="'dept' + (index+1)" class="hidden-checkbox" v-model="item.select">
  148. <label :for="'dept' + (index+1)" class="custom-checkbox"></label>
  149. <label :for="'dept' + (index+1)" class="checkbox-label">
  150. {{ item.dept_name }}
  151. </label>
  152. </div>
  153. </div>
  154. </div>
  155. </van-dialog>
  156. <!-- 状态 -->
  157. <van-dialog v-model="showStatus" title="" width="300" confirm-button-text="确定" @confirm="handleStatusConfirm" @cancel="handleStatusCancel" :show-confirm-button="true" closeOnClickOverlay>
  158. <van-radio-group v-model="statusId">
  159. <div v-for="(item, index) in statusList" :key="index">
  160. <van-radio :name="item.value" style="margin:.3rem 0 .3rem .4rem;font-size:.3rem" icon-size="16px">
  161. <span style="margin-left:.3rem">{{ item.text }}</span>
  162. </van-radio>
  163. </div>
  164. </van-radio-group>
  165. </van-dialog>
  166. </div>
  167. </template>
  168. <script>
  169. import Vue from 'vue';
  170. import VanSkeleton from '@/performance/components/public/VanSkeleton';
  171. import { Picker, ActionSheet, Circle, Tab, Tabs, Skeleton, DropdownMenu, DropdownItem, Progress, Calendar } from 'vant';
  172. import { _debounce } from '@/utils/auth';
  173. import _ from "lodash"
  174. Vue.use(Picker)
  175. .use(ActionSheet)
  176. .use(Circle)
  177. .use(Tab)
  178. .use(Tabs)
  179. .use(Skeleton)
  180. .use(DropdownMenu)
  181. .use(DropdownItem)
  182. .use(Progress)
  183. .use(Calendar);
  184. export default {
  185. data() {
  186. return {
  187. isYou: true,
  188. skeletonLoad: true, // 骨架屏
  189. statementtabshow: false,
  190. statementperson: [],
  191. statementResult: [],
  192. allStatementResult: [],
  193. statementVanTabs: [{ name: '等级分布' }, { name: '正态分布' }],
  194. active: 0,
  195. detailInfo: null,
  196. startTime: "",
  197. endTime: "",
  198. deptIds: [],
  199. rank: "",
  200. rankList: [],
  201. deptList: [],
  202. scoreList: [],
  203. users: [], //考核人员列表
  204. filterUsers: [],
  205. tableData1: [], // 考核中的指标列表,
  206. tableData2: [], // 按单位/目标/聚合指标列表,
  207. filterTableData1: [],
  208. filterTableData2: [],
  209. distributionId: '',
  210. userTotal: 0,
  211. userComplete: 0,
  212. userIncomplete: 0,
  213. infos: [],
  214. paramsText: '',
  215. selectPftiTheEcho: [0, 0], // 选项回显
  216. pullonThePanel: false, // 选项面板
  217. keyword: "",
  218. deptName: '全部部门',
  219. department_list: [],
  220. showDept: false,
  221. statusList: [
  222. {
  223. id: 0, text: "全部", value: "-1"
  224. },
  225. {
  226. id: 1, text: "进行中", value: "0"
  227. },
  228. {
  229. id: 2, text: "已结束", value: "1"
  230. },
  231. ],
  232. showStatus: false,
  233. statusId: "-1",
  234. statusName: "状态",
  235. recordList: [],
  236. userInfo: this.$userInfo(),
  237. gradeLevels: [], // 全局等级配置
  238. cycleOptions: [
  239. { name: "月度", value: "4", id: "4", text: "月度", children: [], className: 'column-1' },
  240. { name: "季度", value: "3", id: "3", text: "季度", children: [], className: 'column-2' },
  241. { name: "半年度", value: "2", id: "2", text: "半年度", children: [], className: 'column-3' },
  242. { name: "年度", value: "1", id: "1", text: "年度", children: [], className: 'column-4' },
  243. { name: "自定义", value: "0", id: "0", text: "自定义", children: [], className: 'column-5' },
  244. ],
  245. headValue: [],
  246. isYou: true,
  247. };
  248. },
  249. components: { VanSkeleton },
  250. watch: {
  251. detailInfo(v) {
  252. this.$nextTick(() => {
  253. this.initData();
  254. })
  255. },
  256. headValue(v) {
  257. let value = ''
  258. if (this.headValue[0] && this.headValue[0] == '0') value = "自定义 / "
  259. if (this.headValue[0] && this.headValue[0] == '1') value = "年度 / "
  260. if (this.headValue[0] && this.headValue[0] == '2') value = "半年度 / "
  261. if (this.headValue[0] && this.headValue[0] == '3') value = "季度 / "
  262. if (this.headValue[0] && this.headValue[0] == '4') value = "月度 / "
  263. let currentOption = this.cycleOptions.find(item => item.value == this.headValue[0])
  264. let optionIndex = this.cycleOptions.findIndex(item => item.value == this.headValue[0])
  265. let childIndex = 0
  266. if(currentOption.children && currentOption.children.length > 0) {
  267. childIndex = currentOption.children.findIndex(child => child.value == this.headValue[1])
  268. value += currentOption.children.find(child => child.value == this.headValue[1]).text;
  269. }
  270. // value += this.cycleOptions.find(item => item.value == this.headValue[1]).label || ''
  271. this.paramsText = value;
  272. this.selectPftiTheEcho = [optionIndex, childIndex] // 周期选项回显
  273. this.getResultAnalyze();
  274. }
  275. },
  276. methods: {
  277. openDetail(item){
  278. this.$router.push({ path: '/newMe', query: { reviewId: item.reviewId, employeeId: item.employeeId } });
  279. },
  280. async initData() {
  281. // this.activeName = '1'
  282. this.deptName = '全部部门'
  283. this.deptIds = [];
  284. this.department_list = [];
  285. this.users = [];
  286. this.filterUsers = [];
  287. this.tableData1 = [];
  288. this.tableData2 = [];
  289. this.filterTableData1 = [];
  290. this.filterTableData2 = [];
  291. let { indicators, startTime, endTime, distribution: { items }, users } = this.detailInfo
  292. this.startTime = startTime;
  293. this.endTime = endTime;
  294. this.scoreList = items;
  295. this.distributionId = this.scoreList[0].id
  296. this.users = users;
  297. this.tableData1 = indicators;
  298. this.tableData1.forEach(item => {
  299. this.users.forEach(user => {
  300. if (user.employeeId == item.employeeId) {
  301. item.departments = user.departments
  302. }
  303. })
  304. if (item.target && item.result) {
  305. item.difference = item.result - item.target
  306. } else {
  307. item.difference = '--'
  308. }
  309. })
  310. this.filterTableData1 = this.tableData1;
  311. if(this.tableData1 && this.tableData1.length > 0) {
  312. let groups = _.groupBy(this.tableData1, item => `${item.title}(_)${item.target === null || item.target === '' ? 'null' : item.target}(_)${item.unit === null || item.unit === '' ? 'null' : item.unit}`);
  313. Object.keys(groups).forEach(key => {
  314. let group = {
  315. title: '',
  316. target: '',
  317. unit: '',
  318. departments: [],
  319. userCount: 0,
  320. scoredCount: 0,
  321. standardCount: 0,
  322. failCount: 0,
  323. standardRate: '--',
  324. totalScore: 0,
  325. totalResult: 0,
  326. avgScore: 0,
  327. avgResult: 0,
  328. standardResultRate: '--'
  329. };
  330. groups[key].forEach(indicator => {
  331. group.title = indicator.title; // 指标名称
  332. group.target = indicator.target; // 目标
  333. group.unit = indicator.unit; // 单位
  334. group.departments = indicator.departments; // 单位
  335. let standardCount = indicator.difference !== '--' && indicator.difference >= 0 ? 1 : 0; //
  336. group.userCount += 1;
  337. group.scoredCount += indicator.score !== null ? 1 : 0;
  338. group.standardCount += standardCount;
  339. group.failCount += standardCount === 1 ? 0 : 1;
  340. if (indicator.score !== null) group.totalScore += indicator.score;
  341. if (indicator.result !== null) group.totalResult += indicator.result;
  342. });
  343. group.standardCount = group.standardCount;
  344. if (group.userCount > 0) {
  345. let rate = Math.floor(group.standardCount / group.userCount * 100 * 0.01);
  346. let avgScore = Math.floor(group.totalScore / group.userCount * 100 * 0.01);
  347. let avgResult = Math.floor(group.totalResult / group.userCount * 100 * 0.01);
  348. group.standardRate = rate > 0 ? `${rate}%` : '--';
  349. group.avgScore = avgScore !== 0 ? avgScore : '--';
  350. group.avgResult = avgResult !== 0 ? avgResult : '--';
  351. if (group.target !== null && group.avgResult !== '--') {
  352. let standardResultRate = Math.floor((group.avgResult - group.target) / group.target * 100 * 0.01 * 100);
  353. group.standardResultRate = standardResultRate !== 0 ? `${standardResultRate}%` : '--';
  354. }
  355. }
  356. this.tableData2.push(group);
  357. this.filterTableData2 = this.tableData2;
  358. })
  359. }
  360. this.userTotal = 0;
  361. this.userComplete = 0;
  362. this.userIncomplete = 0;
  363. let distribution = [];
  364. let userScores = []
  365. this.scoreList.forEach(item => {
  366. item.level = item.name;
  367. item.ratio = item.scale / 100
  368. distribution.push(item)
  369. })
  370. if (this.users && this.users.length > 0) {
  371. this.isYou = true;
  372. this.users.forEach(user => {
  373. this.userTotal++;
  374. this.userComplete += user.status === 1 ? 1 : 0;
  375. user.level = this.findGrade(user.score, this.gradeLevels);
  376. // this.rankList.push(user.level || '未评分')
  377. if(user.score !== null ) {
  378. userScores.push(user.score)
  379. }
  380. })
  381. this.infos = [
  382. { label: "总人数", num: this.userTotal },
  383. { label: "已完成", num: this.userComplete },
  384. { label: "未评分", num: this.userIncomplete },
  385. ]
  386. let scoreResult = this.assignLevels(userScores, distribution); //正态分布
  387. this.users.forEach(item => {
  388. item.dept_list = this.$getEmployeeMapItem(item.employeeId).employee_detail.dept_list
  389. if(item.dept_list && item.dept_list.length > 0) {
  390. item.dept_list.forEach(dept => {
  391. dept.id = dept.dept_id
  392. item.select = false
  393. this.department_list.push(dept)
  394. })
  395. this.department_list = Array.from(
  396. new Set(this.department_list.map(item => JSON.stringify(item)))
  397. ).map(item => JSON.parse(item));
  398. }
  399. scoreResult.forEach(result => {
  400. if (result.scores.includes(item.score)) {
  401. item.scoreResult = result.level
  402. }
  403. })
  404. })
  405. // 按分数排序
  406. this.users = this.users.slice().sort((a, b) => b.score - a.score);
  407. // this.filterUsers = cloneDeep(this.users)
  408. this.filterUsers = this.users
  409. // this.getResult();
  410. let pieData = []
  411. if(this.users && this.users.length > 0) {
  412. pieData = this.gradeLevels.map(item => ({
  413. name: item.name + ' ' + this.users.filter(user => user.level === item.name).length + '人',
  414. value: this.users.filter(user => user.level === item.name).length
  415. }))
  416. this.statmentAnalysesPie(pieData)
  417. }
  418. }else {
  419. this.isYou = false;
  420. }
  421. },
  422. statmentAnalysesPie(data) {
  423. let colors = ['rgb(38, 162, 255)', '#f36f2a', '#fecb09', '#00b6bd', '#e85d53', '#fecb09'];
  424. let legendDataLeng = data.length;
  425. if (legendDataLeng > colors.length) {
  426. let colorsLeng = colors.length;
  427. for (let i = 0; i <= legendDataLeng - colorsLeng; i++) {
  428. colors.push('rgb(' + Math.round(Math.random() * 255) + ',' + Math.round(Math.random() * 255) + ',' + Math.round(Math.random() * 255) + ')');
  429. }
  430. }
  431. // 是否全部 0
  432. const isEmpty = data.every(v => v.value == 0)
  433. if(isEmpty) {
  434. data.forEach(item => item.itemStyle = { color: '#E5E5E5' })
  435. }
  436. const chart = this.$refs.statmentAnalysePie;
  437. if (chart) {
  438. const myChart = this.$echarts.init(chart);
  439. const option = {
  440. legend: [
  441. {
  442. type: 'scroll',
  443. orient: 'vertical',
  444. icon: 'square',
  445. left: '55%',
  446. align: 'left',
  447. top: 'center',
  448. itemGap: 20,
  449. itemWidth: 5, // 图形宽度。
  450. itemHeight: 5, // 图形高度。
  451. // bottom:'50%',
  452. textStyle: {
  453. fontSize: 14,
  454. color: 'rgb(48, 49, 51)'
  455. }
  456. // data: Name
  457. }
  458. ],
  459. color: colors,
  460. emphasis: {
  461. scale: true,
  462. scaleSize: 10,
  463. label: {
  464. formatter: '总人数',
  465. show: true,
  466. fontSize: '20'
  467. }
  468. },
  469. series: [
  470. {
  471. type: 'pie',
  472. radius: ['43%', '53%'],
  473. center: ['30%', '50%'],
  474. avoidLabelOverlap: false,
  475. itemStyle: {
  476. borderRadius: 10,
  477. borderColor: '#fff',
  478. borderWidth: 2
  479. },
  480. label: {
  481. show: false,
  482. position: 'center'
  483. },
  484. labelLine: {
  485. show: false
  486. },
  487. data: data.reverse()
  488. }
  489. ]
  490. };
  491. myChart.setOption(option);
  492. setTimeout(() => {
  493. myChart.resize();
  494. }, 200);
  495. }
  496. },
  497. // 查找分数对应的等级
  498. findGrade(score, gradeLevels) {
  499. for (let i = 0; i < gradeLevels.length; i++) {
  500. if (score && score >= gradeLevels[i].min && score && score <= gradeLevels[i].max) {
  501. return gradeLevels[i].name; // 返回对应的等级描述
  502. } else if (score && score <= gradeLevels[0].min) {
  503. return gradeLevels[0].name; // 返回对应的等级描述
  504. } else if (score && score >= gradeLevels[gradeLevels.length - 1].max) {
  505. return gradeLevels[gradeLevels.length - 1].name; // 返回对应的等级描述
  506. }
  507. }
  508. return "未评级"; // 如果分数不在任何范围内
  509. },
  510. assignLevels(scores, levelConfigs) {
  511. // 降序排序并去重(假设分数不重复,可省略去重)
  512. const sortedScores = [...scores].sort((a, b) => b - a);
  513. const total = sortedScores.length;
  514. if (total === 0) return [];
  515. // 是否全部 0
  516. const isEmpty = sortedScores.every(v => v == 0 || v == null)
  517. if(isEmpty) return [];
  518. // 归一化处理比例
  519. const totalRatio = levelConfigs.reduce((sum, cfg) => sum + cfg.ratio, 0);
  520. const normalized = levelConfigs.map(cfg => cfg.ratio / totalRatio);
  521. // 计算每个等级的初始人数
  522. let counts = normalized.map(ratio => Math.floor(total * ratio));
  523. let remainder = total - counts.reduce((sum, c) => sum + c, 0);
  524. // 分配剩余人数,按优先级顺序
  525. let idx = 0;
  526. while (remainder > 0 && idx < counts.length) {
  527. counts[idx]++;
  528. remainder--;
  529. idx++;
  530. }
  531. // 构建结果:按人数切割数组
  532. let start = 0;
  533. return counts.map((count, i) => {
  534. const end = start + count;
  535. const levelScores = sortedScores.slice(start, end);
  536. start = end;
  537. return {
  538. level: levelConfigs[i].level,
  539. scores: levelScores
  540. };
  541. });
  542. },
  543. statementTabs() {},
  544. openPanel(){
  545. this.pullonThePanel = true;
  546. this.$nextTick(() => {
  547. this.theEchoVanPicker()
  548. })
  549. },
  550. // 考核包搜索
  551. onConfirm (data, list) { // 确认
  552. this.selectPftiTheEcho = list
  553. let cycle = this.cycleOptions[list[0]].value
  554. let time = this.cycleOptions[list[0]].children[list[1]].value
  555. this.headValue = [cycle, time]
  556. this.pullonThePanel = false
  557. },
  558. // 获取全局等级设置
  559. async getAllLevelSet() {
  560. let res = await this.$axiosUser('get', 'api/pro/per/user/base_config')
  561. let data = res.data.data;
  562. let levels = data.level_scope.levels;
  563. let gradeLevels = [];
  564. let max = 0;//最大值
  565. let colorList = ['#5F7294', '#08EEA1', '#f56c6c', '#0887EE', '#92EE08', '#f56c6c']
  566. if (levels && levels.length > 0) {
  567. levels.forEach((item, index) => {
  568. var obj;
  569. if (index == 0) {
  570. obj = { name: item.name, max: Number(item.value), min: 0 };
  571. } else {
  572. obj = { name: item.name, max: Number(item.value), min: max };//当不是第一个等级时,最小值为上一个的最大值
  573. }
  574. obj.color = colorList[index]
  575. max = item.value;
  576. gradeLevels.push(obj);
  577. })
  578. this.gradeLevels = gradeLevels;
  579. }
  580. },
  581. // 获取结果分析数据
  582. getResultAnalyze() {
  583. let url = `/performance/statistics/cycle/${this.$userInfo().site_id}/${this.headValue[0]}`
  584. if (!this.headValue[1]) return
  585. this.$axiosUser("get", url, { value: this.headValue[1] }).then(res => {
  586. this.detailInfo = res.data.data
  587. this.skeletonLoad = false
  588. })
  589. },
  590. getRecords(cycle) {
  591. if (cycle < 0) {
  592. cycle = 0
  593. return
  594. }
  595. // 周期种类 0-自定义 1-年度 2-半年度 3-季度 4-月度
  596. let url = `/performance/statistics/cycle/info/${this.$userInfo().site_id}/${cycle}`
  597. this.$axiosUser("get", url, {}).then(res => {
  598. let { data: { data: { items, cycleType } } } = res
  599. if (items && items.length > 0) {
  600. items.forEach(item => {
  601. item.name = item.remark
  602. item.text = item.remark
  603. item.id = item.value
  604. })
  605. let index = parseInt(4 - cycle)
  606. this.cycleOptions[index].children = items
  607. this.headValue = [cycle + '', this.cycleOptions[index].children[[0]].value]
  608. // if (this.headValue[1]) this.placeholder = this.options[parseInt(cycle)].children[[0]].label || ''
  609. } else {
  610. this.getRecords(parseInt(cycle) - 1) // 递归周期类型,获取考核数据,优先按月,季,半年度,年度来调用
  611. }
  612. })
  613. },
  614. theEchoVanPicker() {
  615. // 回显
  616. this.$refs.van_picker.setIndexes(this.selectPftiTheEcho);
  617. },
  618. onCancel() {
  619. // 取消
  620. this.pullonThePanel = false;
  621. },
  622. keyVal: _debounce(function() {
  623. if(!this.keyword) this.filterUsers = this.recordList
  624. // let filterUsers = []
  625. if(this.recordList && this.recordList.length > 0) {
  626. this.filterUsers = this.recordList.filter(item => item.employeeName.includes(this.keyword.trim()))
  627. }
  628. }),
  629. handleConfirm() {
  630. this.deptIds = this.department_list.filter(item => item.select).map(item => item.dept_id)
  631. if(this.deptIds && this.deptIds.length > 0) {
  632. this.deptName = ''
  633. this.department_list.filter(item => item.select).forEach(dept => {
  634. this.deptName += dept.dept_name + ","
  635. })
  636. this.deptName = this.deptName.substring(0, this.deptName.length - 1)
  637. // this.filterUsers = this.users.filter((item) => {
  638. // const departmentMatch = this.deptIds.length === 0 || this.deptIds.some((depId) => item.departments.some(dep => dep.id == depId));
  639. // return departmentMatch;
  640. // });
  641. }else {
  642. this.deptName = '全部部门'
  643. }
  644. this.filteredData()
  645. this.showDept = false;
  646. },
  647. // 实时过滤后数据
  648. filteredData() {
  649. this.filterUsers = this.users.filter((item) => {
  650. // 部门筛选:如果选了部门,则过滤;没选则不过滤
  651. const departmentMatch = this.deptIds.length === 0 || this.deptIds.some((depId) => item.departments.some(dep => dep.id == depId));
  652. // 状态筛选:如果选了“全部”,则不过滤;否则按状态过滤
  653. const statusMatch = this.statusId == '-1' || item.status == this.statusId;
  654. return departmentMatch && statusMatch;
  655. });
  656. },
  657. handleCancel() {
  658. this.showDept = false;
  659. },
  660. handleStatusConfirm() {
  661. this.filteredData();
  662. this.statusName = this.statusList.find(item => item.value == this.statusId).text || '状态'
  663. this.showStatus = false
  664. },
  665. handleStatusCancel() {
  666. this.showStatus = false;
  667. },
  668. getColumnsInfo(cycle) {
  669. if (cycle < 0) {
  670. cycle = 0
  671. return
  672. }
  673. // this.loading = true
  674. // 周期种类 0-自定义 1-年度 2-半年度 3-季度 4-月度
  675. let url = `/performance/statistics/cycle/info/${this.$userInfo().site_id}/${cycle}`
  676. this.$axiosUser("get", url, {}).then(res => {
  677. // this.loading = false;
  678. let { data: { data: { items, cycleType } } } = res
  679. let index = parseInt(4 - cycle)
  680. this.cycleOptions[index].children = items
  681. if (items && items.length > 0) {
  682. items.forEach(item => {
  683. item.name = item.remark
  684. item.text = item.remark
  685. item.id = item.value
  686. })
  687. let cycle = this.cycleOptions[index].value
  688. let time = this.cycleOptions[index].children[[0]].value
  689. } else {
  690. this.cycleOptions[index].children = [
  691. {name: '', text: '', value: '', id: ''}
  692. ]
  693. }
  694. })
  695. },
  696. },
  697. mounted() {
  698. this.$nextTick(async () => {
  699. // 获取全局等级
  700. await this.getAllLevelSet();
  701. // 获取周期列表数据
  702. await this.getColumnsInfo(4); // 月度
  703. await this.getColumnsInfo(3);
  704. await this.getColumnsInfo(2);
  705. await this.getColumnsInfo(1);
  706. await this.getColumnsInfo(0);
  707. this.getRecords(4);
  708. })
  709. },
  710. created() {
  711. }
  712. };
  713. </script>
  714. <style scoped lang="less">
  715. /* 父容器:垂直居中的关键 */
  716. .checkbox-wrapper {
  717. display: flex;
  718. align-items: center; /* 垂直居中 */
  719. gap: 8px; /* 图标与文字间距 */
  720. margin-bottom: 0.2rem;
  721. font-size: 0.28rem;
  722. color: #89919F;
  723. }
  724. /* 隐藏原生的 checkbox */
  725. .hidden-checkbox {
  726. position: absolute;
  727. opacity: 0;
  728. width: 0;
  729. height: 0;
  730. }
  731. /* 自定义 checkbox 框 */
  732. .custom-checkbox {
  733. position: relative;
  734. width: 14px;
  735. height: 14px;
  736. border: 1px solid #dcdfe6;
  737. border-radius: 4px;
  738. cursor: pointer;
  739. transition: all .2s;
  740. flex-shrink: 0; /* 防止被压缩 */
  741. }
  742. /* 选中背景 */
  743. .hidden-checkbox:checked + .custom-checkbox {
  744. background: #409eff;
  745. border-color: #409eff;
  746. }
  747. /* 选中对勾 */
  748. .hidden-checkbox:checked + .custom-checkbox::after {
  749. content: "";
  750. position: absolute;
  751. left: 0.06rem;
  752. top: 0.02rem;
  753. width: 5px;
  754. height: 8px;
  755. border: solid #fff;
  756. border-width: 0 2px 2px 0;
  757. transform: rotate(45deg);
  758. }
  759. /* label 文字样式(可选) */
  760. .checkbox-label {
  761. cursor: pointer;
  762. user-select: none;
  763. line-height: 1.4;
  764. }
  765. .van-action-sheet__content /deep/ .van-picker-column.column-1 {
  766. flex: 0 0 240px !important;
  767. max-width: 240px !important;
  768. }
  769. .color-red {
  770. padding: 0.05rem 0.2rem;
  771. box-sizing: border-box;
  772. background-color: #fef0f0;
  773. border: 0.02rem solid #fde2e2;
  774. color: #f56c6c !important;
  775. }
  776. .color-green {
  777. padding: 0.05rem 0.2rem;
  778. box-sizing: border-box;
  779. background-color: #f0f9eb;
  780. border: 0.02rem solid #e1f3d8;
  781. color: #67c23a !important;
  782. }
  783. .color-gray {
  784. padding: 0.05rem 0.2rem;
  785. box-sizing: border-box;
  786. background-color: #f4f4f5;
  787. border: 0.02rem solid #e9e9eb;
  788. color: #909399 !important;
  789. }
  790. .dept-list {
  791. padding: 0.2rem;
  792. box-sizing: border-box;
  793. .dept-item {
  794. box-sizing: border-box;
  795. padding-left: 0.3rem;
  796. height: 0.8rem;
  797. &-name {
  798. font-size: 0.28rem;
  799. color: #89919F;
  800. }
  801. }
  802. }
  803. .statementHnum {
  804. display: flex;
  805. flex-wrap: wrap;
  806. width: 100%;
  807. padding: 0 0.2rem;
  808. margin-top: 0.2rem;
  809. .Hnumvfor {
  810. flex: 0 0 calc((100% - 1rem) / 4);
  811. position: relative;
  812. background-color: #ECF5FF;
  813. border-radius: 3px;
  814. text-align: center;
  815. margin: 0 0.2rem 0.2rem 0;
  816. padding: 0.1rem 0.2rem;
  817. box-sizing: border-box;
  818. &:nth-child(4n) {
  819. /* 去除第5n个的margin-right */
  820. margin-right: 0;
  821. }
  822. .smHnLnum {
  823. font-size: 0.42rem;
  824. color: #00a1ff;
  825. }
  826. .smHnLtit {
  827. font-size: 0.26rem;
  828. color: #8a8a8a;
  829. }
  830. }
  831. }
  832. .selectItem {
  833. height: 0.8rem;
  834. line-height: 0.8rem;
  835. background-color: #fff;
  836. border-top: 0.02rem solid #f1f1f1;
  837. text-align: center;
  838. font-size: 0.28rem;
  839. i {
  840. margin: 0.23rem 0 0 0.1rem;
  841. }
  842. }
  843. header {
  844. .selector {
  845. z-index: 1;
  846. display: flex;
  847. align-items: center;
  848. justify-content: center;
  849. width: 100%;
  850. height: 0.8rem;
  851. line-height: 0.8rem;
  852. background-color: #fff;
  853. text-align: center;
  854. font-size: 0.28rem;
  855. i {
  856. margin: 0 0 0 0.1rem;
  857. color: #c3c3c3;
  858. }
  859. span {
  860. max-width: 3.5rem;
  861. overflow: hidden;
  862. }
  863. }
  864. }
  865. .all {
  866. height: calc(100% - 1.92rem) !important;
  867. position: relative !important;
  868. background-color: #f5f7fa;
  869. .statementHead {
  870. width: 100%;
  871. text-align: center;
  872. // padding-top: 0.35rem;
  873. background-color: #fff;
  874. .statementpropnum {
  875. background-color: #f5f7fa;
  876. padding: 0.3rem 0 0.1rem;
  877. .propnumrel1 {
  878. /deep/ .van-progress__pivot {
  879. margin-left: -0.11rem;
  880. }
  881. }
  882. .propnumrel {
  883. position: relative;
  884. padding: 0 0.15rem;
  885. /deep/ .van-progress {
  886. border-radius: 0.5rem;
  887. .van-progress__portion {
  888. border-radius: 0.14rem;
  889. .van-progress__pivot {
  890. border-radius: 50%;
  891. min-width: 0.33rem !important;
  892. height: 0.33rem;
  893. right: 0;
  894. // margin-left: -.11rem;
  895. }
  896. }
  897. }
  898. .propnumcol {
  899. position: absolute;
  900. top: 0;
  901. right: 0.4rem;
  902. font-size: 0.25rem;
  903. color: #fff;
  904. }
  905. .numelzi {
  906. font-size: 0.26rem;
  907. margin: 0.25rem 0;
  908. .numelzh {
  909. color: #000000;
  910. }
  911. .numelzl {
  912. color: #0597ff;
  913. display: flex;
  914. align-items: center;
  915. justify-content: center;
  916. i {
  917. margin: 0.06rem 0 0 0.02rem;
  918. }
  919. }
  920. }
  921. }
  922. }
  923. .circle {
  924. margin-top: 0.7rem;
  925. .circletit {
  926. font-size: 0.24rem;
  927. color: #6f6f6f;
  928. }
  929. .circleclic {
  930. color: #4ba0f1;
  931. .circleclics1 {
  932. font-size: 0.42rem;
  933. display: inline-block;
  934. margin: 0.1rem 0;
  935. }
  936. .circleclics2 {
  937. font-size: 0.25rem;
  938. display: flex;
  939. justify-content: center;
  940. i {
  941. margin: 0.06rem 0 0 0.02rem;
  942. }
  943. }
  944. }
  945. }
  946. }
  947. .statementFoot {
  948. background-color: #fff;
  949. margin-top: 0.2rem;
  950. .statmentperson {
  951. padding: 0.25rem 0.2rem;
  952. font-size: 0.32rem;
  953. box-sizing: border-box;
  954. .score-result {
  955. width: 0.4rem;
  956. height: 0.4rem;
  957. border: 0.02rem solid #409EFF;
  958. color: #409EFF;
  959. border-radius: 50%;
  960. font-size: 0.26rem;
  961. text-align: center;
  962. line-height: 0.4rem;
  963. margin-right: 0.1rem;
  964. }
  965. }
  966. }
  967. }
  968. .takI {
  969. font-size: 0.27rem;
  970. color: #f7b461;
  971. i {
  972. margin: 0.1rem 0.05rem 0 0;
  973. }
  974. }
  975. .statmentAnalysePie {
  976. position: relative;
  977. .PiePropNum {
  978. width: 1.5rem;
  979. text-align: center;
  980. position: absolute;
  981. left: 20%;
  982. top: 42%;
  983. font-size: 0.3rem;
  984. }
  985. }
  986. .review-status {
  987. // padding: 0.01rem 0.1rem;
  988. width: 1.2rem;
  989. height: 0.4rem;
  990. text-align: center;
  991. line-height: 0.4rem;
  992. color: #fff;
  993. border-radius: 2px;
  994. font-size: 0.26rem;
  995. background-color: #67c23a;
  996. margin-right: 0.14rem;
  997. margin-bottom: 0.1rem;
  998. }
  999. .indicator-list-title {
  1000. padding: 0.2rem 0.2rem 0 0.2rem;
  1001. box-sizing: border-box;
  1002. font-size: 0.28rem;
  1003. }
  1004. .indicator-list {
  1005. padding: 0.2rem;
  1006. box-sizing: border-box;
  1007. .indicator-item {
  1008. background-color: white;
  1009. margin-bottom: 0.2rem;
  1010. padding: 0.2rem;
  1011. box-sizing: border-box;
  1012. border-radius: 8px;
  1013. .indicator-item-info {
  1014. display: flex;
  1015. align-items: center;
  1016. justify-content: space-between;
  1017. font-size: 0.28rem;
  1018. margin-bottom: 0.2rem;
  1019. .indicator-info-label {
  1020. width: 2rem;
  1021. color: black;
  1022. }
  1023. .indicator-info-value {
  1024. flex: 1;
  1025. text-align: right;
  1026. white-space: nowrap;
  1027. overflow: hidden;
  1028. text-overflow: ellipsis;
  1029. color: #89919F;
  1030. }
  1031. }
  1032. }
  1033. }
  1034. .van-picker__columns .van-picker-column:nth-child(2) {
  1035. background-color: red !important;
  1036. }
  1037. </style>