ProcessTracking.vue 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  1. <template>
  2. <div class="record-container">
  3. <div class="main-content">
  4. <div class="search-box">
  5. <div class="flex-box-ce">
  6. <el-select style="width: 120px;" v-model="params.cycleType" placeholder="请选择周期种类"
  7. @change="changeCycleType">
  8. <el-option v-for="item in cycleOptions" :key="item.id" :label="item.name" :value="item.id">
  9. </el-option>
  10. </el-select>
  11. <el-date-picker class="cursor" style="width: 150px; margin-left: 10px;" v-model="params.year"
  12. type="year" placeholder="选择年" value-format="yyyy" @change="changeYear">
  13. </el-date-picker>
  14. <el-date-picker class="cursor" v-if="params.cycleType == '0'"
  15. style="width: 300px; margin-left: 10px;" v-model="dateRange" type="daterange"
  16. range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" format="yyyy-MM-dd"
  17. value-format="yyyy-MM-dd"></el-date-picker>
  18. <el-select class="cursor" v-if="params.cycleType == '2'" style="width: 150px; margin-left: 10px;"
  19. v-model="cycleValue" placeholder="请选择半年度" @change="changeCycleValue">
  20. <el-option label="上半年" value="1">
  21. </el-option>
  22. <el-option label="下半年" value="2">
  23. </el-option>
  24. </el-select>
  25. <el-select class="cursor" v-if="params.cycleType == '3'" style="width: 150px; margin-left: 10px;"
  26. v-model="cycleValue" placeholder="请选择季度" @change="changeCycleValue">
  27. <el-option label="第一季度" value="1">
  28. </el-option>
  29. <el-option label="第二季度" value="2">
  30. </el-option>
  31. <el-option label="第三季度" value="3">
  32. </el-option>
  33. <el-option label="第四季度" value="4">
  34. </el-option>
  35. </el-select>
  36. <el-select class="cursor" v-if="params.cycleType == '4'" style="width: 150px; margin-left: 10px;"
  37. v-model="cycleValue" placeholder="请选择月度" @change="changeCycleValue">
  38. <el-option label="一月" value="1">
  39. </el-option>
  40. <el-option label="二月" value="2">
  41. </el-option>
  42. <el-option label="三月" value="3">
  43. </el-option>
  44. <el-option label="四月" value="4">
  45. </el-option>
  46. <el-option label="五月" value="5">
  47. </el-option>
  48. <el-option label="六月" value="6">
  49. </el-option>
  50. <el-option label="七月" value="7">
  51. </el-option>
  52. <el-option label="八月" value="8">
  53. </el-option>
  54. <el-option label="九月" value="9">
  55. </el-option>
  56. <el-option label="十月" value="10">
  57. </el-option>
  58. <el-option label="十一月" value="11">
  59. </el-option>
  60. <el-option label="十二月" value="12">
  61. </el-option>
  62. </el-select>
  63. <div class="dept_wdiv flex-box-ce" style="margin: 0 0 0 10px;">
  64. <div class="dept_inp" @click="show_dept_selector = true">
  65. <span v-if="deptVisibleName != ''">{{ deptVisibleName }}</span>
  66. <span v-else style="color: #b9b9b9;">选择部门</span>
  67. </div>
  68. <i class="el-icon-arrow-down"></i>
  69. </div>
  70. <el-select class="cursor" style="width: 260px; margin-left: 10px;" v-model="selectEmployeeId"
  71. filterable clearable multiple placeholder="员工姓名搜索" @change="changeEmployeeIds">
  72. <el-option v-for="item in employees" :key="item.id" :label="item.name"
  73. :value="item.id"></el-option>
  74. </el-select>
  75. </div>
  76. <el-button type="primary" @click="exportToExcel('过程分析', '#myTable')" size="small"
  77. :loading="downloadLoading">导出明细</el-button>
  78. </div>
  79. <!-- 水平线 -->
  80. <div class="line"></div>
  81. <div class="flex-box-ce flex-d-wrap"
  82. style="justify-content: center; padding: 20px 0; box-sizing: border-box;">
  83. <div v-for="(item, index) in generalizeList" :key="index" @click="chooseExamineStatus(item.name)"
  84. class="generalize-item">
  85. <div class=" fontColorC">{{ item.name }}</div>
  86. <div style="font-size: 36px; font-weight: 600;margin: 5px 0;">{{ item.val }}</div>
  87. <div style="width: 60px;height: 2px;" :style="{ backgroundColor: item.color }"></div>
  88. </div>
  89. <div v-for="(item, index) in gradeLevels" @click="chooseExamineLevel(item.name)" :key="item.name"
  90. class="generalize-item">
  91. <div class="fontColorC">{{ item.name }}</div>
  92. <div style="font-size: 36px; font-weight: 600;margin: 5px 0;">
  93. {{tableData.filter(user => user.levelName === item.name).length}}
  94. </div>
  95. <div style="width: 60px;height: 2px;" :style="{ backgroundColor: item.color }"></div>
  96. </div>
  97. </div>
  98. <div class="table-box">
  99. <el-table id="myTable" :data="filteredData" style="width: 100%; height: auto;"
  100. :header-cell-style="{ background: '#f5f7fa' }" border stripe>
  101. <el-table-column prop="date" label="考核时间" align="center"></el-table-column>
  102. <el-table-column prop="employeeName" label="员工姓名" align="center"></el-table-column>
  103. <el-table-column prop="department" label="部门" width="300" align="center">
  104. <template slot-scope="scope">
  105. {{ scope.row.department | formatDeptName }}
  106. </template>
  107. </el-table-column>
  108. <el-table-column prop="status" label="考核状态" align="center">
  109. <template slot-scope="scope">
  110. <el-tag v-if="scope.row.status" type="success">
  111. 已完成
  112. </el-tag>
  113. <el-tag v-else="scope.row.status" type="warning">
  114. 进行中
  115. </el-tag>
  116. </template>
  117. </el-table-column>
  118. <el-table-column prop="score" label="评分" align="center">
  119. <template slot-scope="scope">
  120. <el-tag v-if="!scope.row.score" type="info">
  121. 未评分
  122. </el-tag>
  123. <el-tag v-else>
  124. {{ scope.row.score }}
  125. </el-tag>
  126. </template>
  127. </el-table-column>
  128. <el-table-column prop="levelName" label="等级" align="center">
  129. <template slot-scope="scope">
  130. <el-tag type="info">
  131. {{ scope.row.levelName || '--' }}
  132. </el-tag>
  133. </template>
  134. </el-table-column>
  135. <el-table-column label="操作" align="center">
  136. <template slot-scope="scope">
  137. <el-button type="text" @click="getTemplateDetails(scope.row)">查看明细</el-button>
  138. </template>
  139. </el-table-column>
  140. </el-table>
  141. <div style="height: 50px;"></div>
  142. </div>
  143. </div>
  144. <!-- 关联okr -->
  145. <TargetListComp v-if="targetDialogVisible" :dialogVisible="targetDialogVisible" :ids="okrs"
  146. @close="closeTargetList">
  147. </TargetListComp>
  148. <!-- 部门选择 -->
  149. <EmployeeSelector :title="'选择部门'" :isChecKedAll="false" :can_select_employee="false" :can_select_dept="true"
  150. :dept_children="false" :selected="dept_selected" :visible.sync="show_dept_selector"
  151. @confirm="dept_confirm" />
  152. <el-dialog title="员工绩效详情" :visible.sync="detailDialogVisible" @close="handleClose" :close-on-click-modal="false"
  153. :close-on-press-escape="true" center fullscreen :show-close="false">
  154. <div style=" width: 100%; height: 100%; position: relative;">
  155. <el-button round style="position: absolute; top: -65px; left: 0px; z-index: 99;"
  156. @click="detailDialogVisible = false">返回</el-button>
  157. <!-- 我的考核 -->
  158. <MyPerformance v-if="detailDialogVisible" :reviewId="reviewId" :sendEmployeeId='sendEmployeeId' />
  159. </div>
  160. </el-dialog>
  161. </div>
  162. </template>
  163. <script>
  164. let that;
  165. import { mapGetters } from 'vuex';
  166. import moment from 'moment';
  167. import TargetListComp from "@/performance/views/assessManagement/TargetListComp.vue"; // 关联OKR弹框
  168. import EmployeeSelector from '@/components/EmployeeSelector'; // 部门选择
  169. import MyPerformance from './MyPerformance'; // 我的考核
  170. import XLSX from 'xlsx';
  171. import FileSaver from 'file-saver';
  172. export default {
  173. components: {
  174. TargetListComp,
  175. EmployeeSelector,
  176. MyPerformance
  177. },
  178. data() {
  179. return {
  180. employees: this.$getEmployeeMap(), // 员工列表
  181. isLeftShow: true,
  182. isRightShow: false,
  183. employeeName: '',
  184. targetDialogVisible: false,
  185. total: 0,
  186. loading: false,
  187. checked: [],
  188. status: -1,
  189. cycleValue: '1',
  190. downloadLoading: false,
  191. show_dept_selector: false,
  192. deptVisibleName: '',
  193. dept_selected: { dept: [], employee: [] },
  194. cycleOptions: [
  195. { name: "月度", id: '4' },
  196. { name: "季度", id: '3' },
  197. { name: "半年度", id: '2' },
  198. { name: "年度", id: '1' },
  199. { name: "未定义", id: '0' },
  200. ],
  201. options: [
  202. {
  203. value: -1,
  204. label: '全部'
  205. }, {
  206. value: '0',
  207. label: '未完成'
  208. }, {
  209. value: '1',
  210. label: '已完成'
  211. }
  212. ],
  213. reviewId: "",
  214. sendEmployeeId: "",
  215. reviewPackageId: 0,
  216. selectedReviewPackageId: '',
  217. cateIds: [],
  218. scoreList: [],
  219. distributionId: '',
  220. gradeLevels: [],
  221. infos: [],
  222. generalizeList: [
  223. { name: '考核人数', val: 0, color: '#FF9600' },
  224. { name: '已结束', val: 0, color: '#409EFF' },
  225. { name: '进行中', val: 0, color: '#67c23a' },
  226. { name: '面谈中', val: 0, color: '#f56c6c' },
  227. // { name: '已取消', val: 0, color: '#5F7294' },
  228. // { name: '未开始', val: 0, color: '#08EEA1' },
  229. // { name: '暂停中', val: 0, color: '#f56c6c' }
  230. ],
  231. treeData: [],
  232. singleSelection: true, // 是否单选
  233. checkedKeys: [],
  234. defaultProps: {
  235. children: 'children',
  236. label: 'label'
  237. },
  238. dept_select_id: '',
  239. keyword: '',
  240. deptList: [], // 部门列表 - 树形结构
  241. dept_list: [], // 部门列表
  242. employeeMap: this.$getEmployeeMap(), // 员工列表
  243. loading: false,
  244. total: 0,
  245. tableData: [],
  246. filteredData: [],
  247. selectEmployeeId: [],
  248. // 周期类型 0-未定义 1-年度 2-半年度 3-季度 4-月度
  249. params: {
  250. cycleType: "4",
  251. startDate: '',
  252. endDate: '',
  253. deptIds: '',
  254. year: ''
  255. },
  256. status: '',
  257. level: '',
  258. dialogVisible: false,
  259. organSelectDialog: false,
  260. gradeLevels: [],
  261. examineStatus: [
  262. { label: "考核中", value: "0" },
  263. { label: "已结束", value: "1" },
  264. { label: "面谈", value: "2" }
  265. ],
  266. userInfo: null,
  267. isContentLeft: true,
  268. isContentRight: true,
  269. chooseUserIds: [],
  270. employeeName: '',
  271. isFold: false,
  272. dateRange: this.getMonthFirstAndLastDay(), // 设置默认值
  273. detailDialogVisible: false,
  274. pickerOptions: {
  275. shortcuts: [{
  276. text: '最近一周',
  277. onClick(picker) {
  278. const end = new Date();
  279. const start = new Date();
  280. start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
  281. picker.$emit('pick', [start, end]);
  282. }
  283. }, {
  284. text: '最近一个月',
  285. onClick(picker) {
  286. const end = new Date();
  287. const start = new Date();
  288. start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
  289. picker.$emit('pick', [start, end]);
  290. }
  291. }, {
  292. text: '最近三个月',
  293. onClick(picker) {
  294. const end = new Date();
  295. const start = new Date();
  296. start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
  297. picker.$emit('pick', [start, end]);
  298. }
  299. }]
  300. },
  301. }
  302. },
  303. watch: {
  304. dateRange(v) {
  305. console.log(v);
  306. }
  307. },
  308. async created() {
  309. that = this;
  310. await this.getAllSet();
  311. this.getDefaultTime();
  312. },
  313. //
  314. computed: {
  315. ...mapGetters(['user_info']),
  316. },
  317. filters: {
  318. formatDate(val) {
  319. if (val) return moment(val).format('YYYY-MM-DD')
  320. else return "--"
  321. },
  322. filterProgress(list) {
  323. if (list && list.length > 0) {
  324. let sum = 0;
  325. list.forEach(item => {
  326. if (item.status == 1) sum += 1 // 完成的个数
  327. })
  328. return Number(sum / list.length) * 100
  329. }
  330. },
  331. formatDeptName(val) {
  332. let str = '';
  333. if (val && val.length > 0) {
  334. val.forEach(dept => {
  335. str += dept.dept_name + ","
  336. })
  337. str = str.substr(0, str.length - 1)
  338. } else {
  339. str = "--"
  340. }
  341. return str
  342. }
  343. },
  344. methods: {
  345. getMonthFirstAndLastDay() {
  346. const today = new Date();
  347. const year = today.getFullYear();
  348. const month = today.getMonth(); // 月份从 0 开始,需要加 1
  349. // 获取当月第一天
  350. const firstDay = new Date(year, month, 1);
  351. const firstDayStr = this.formatDate(firstDay);
  352. // 获取当月最后一天
  353. const lastDay = new Date(year, month + 1, 0); // 下个月的第 0 天是当前月的最后一天
  354. const lastDayStr = this.formatDate(lastDay);
  355. // console.log(firstDayStr)
  356. // console.log(lastDayStr)
  357. return [firstDayStr, lastDayStr];
  358. },
  359. formatDate(date) {
  360. const year = date.getFullYear();
  361. const month = String(date.getMonth() + 1).padStart(2, '0');
  362. const day = String(date.getDate()).padStart(2, '0');
  363. return `${year}-${month}-${day}`;
  364. },
  365. // 周期筛选
  366. changeCycleType(v) {
  367. if (v !== '0') {
  368. this.params.startDate = ''
  369. this.params.endDate = ''
  370. } else if (v == 1) {
  371. // console.log("-----------", 'cycleValue' in this.params)
  372. this.params.year = this.currentYear
  373. this.cycleValue = ''
  374. if ('cycleValue' in this.params) delete this.params.cycleValue
  375. } else if (v == 2) {
  376. this.params.year = this.currentYear
  377. this.params = { ...this.params, cycleValue: this.cycleValue }
  378. } else if (v == 3) {
  379. this.params.year = this.currentYear
  380. this.params = { ...this.params, cycleValue: this.cycleValue }
  381. } else if (v == 4) {
  382. this.params.year = this.currentYear
  383. this.params = { ...this.params, cycleValue: this.cycleValue }
  384. }
  385. this.getRecords();
  386. },
  387. // 周期筛选
  388. changeDate(v) {
  389. if (v && v.length > 0) {
  390. this.dateRange[0] = v[0]
  391. this.dateRange[1] = v[1]
  392. this.params.startDate = v[0]
  393. this.params.endDate = v[1]
  394. this.getRecords();
  395. }
  396. },
  397. changeYear(v) {
  398. this.params.year = v;
  399. this.params.startDate = ''
  400. this.params.endDate = ''
  401. this.getRecords();
  402. },
  403. changeCycleValue(v) {
  404. this.params = { ...this.params, cycleValue: this.cycleValue }
  405. this.getRecords();
  406. },
  407. dept_confirm(data) {
  408. //部门选择
  409. this.dept_selected = { dept: [], employee: [] };
  410. this.deptVisibleName = '';
  411. let deptList_id = [];
  412. if (data.dept !== null && data.dept.length != 0) {
  413. this.dept_selected = data;
  414. data.dept.forEach((element, index) => {
  415. deptList_id.push(element.dept_id);
  416. this.deptVisibleName += element.dept_name;
  417. if (data.dept.length - index > 1) {
  418. this.deptVisibleName += ',';
  419. }
  420. });
  421. }
  422. this.params.deptIds = deptList_id.toString()
  423. this.getRecords();
  424. },
  425. // 员工筛选
  426. changeEmployeeIds(v) {
  427. this.selectEmployeeId = v
  428. this.applyFilters();
  429. },
  430. // 考核状态筛选
  431. chooseExamineStatus(name) {
  432. this.status = name
  433. this.applyFilters();
  434. },
  435. // 考核等级筛选
  436. chooseExamineLevel(name) {
  437. this.level = name
  438. this.applyFilters();
  439. },
  440. // 应用筛选条件
  441. applyFilters() {
  442. this.filteredData = this.tableData.filter((item) => {
  443. const employeeMatch = this.selectEmployeeId.length === 0 || this.selectEmployeeId.includes(item.employeeId);
  444. // let statusMatch = true;
  445. // if (this.status == '考核人数') statusMatch = true
  446. // if (this.status == '进行中') statusMatch = item.status == 0
  447. // if (this.status == '已结束') statusMatch = item.status == 1
  448. // if (this.status == '面谈中') statusMatch = item.status == 2
  449. // let levelMatch = !this.level || this.level && item.levelName == this.level
  450. // console.log(levelMatch)
  451. return employeeMatch
  452. // return employeeMatch && statusMatch && levelMatch;
  453. });
  454. },
  455. // 时间格式化
  456. formatDate(val) {
  457. if (val) return moment(val).format('YYYY-MM-DD')
  458. else return "--"
  459. },
  460. // 隐藏部门菜单
  461. toggle() {
  462. this.isFold = !this.isFold
  463. },
  464. // 还原部门菜单
  465. back() {
  466. this.isLeftShow = true;
  467. this.isRightShow = false;
  468. },
  469. filterNode(value, data) {
  470. if (!value) return true;
  471. return data.label.indexOf(value) !== -1;
  472. },
  473. handleNodeClick(node) {
  474. this.dept_select_id = node.id
  475. },
  476. getTemplateDetails(row) {
  477. this.reviewId = row.reviewId
  478. this.sendEmployeeId = row.employeeId
  479. this.detailDialogVisible = true
  480. // this.$emit('changeCurrentId', { currentId: '2', reviewId })
  481. },
  482. // 获取当前时间
  483. getDefaultTime() {
  484. const date = new Date();
  485. this.currentYear = date.getFullYear()
  486. this.currentMonth = date.getMonth() + 1; // 注意月份从0开始,所以需要加1
  487. const now = new Date(); // 获取当前时间
  488. let startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0); // 当前月的第一天
  489. let endOfMonth = new Date(startOfMonth.getFullYear(), startOfMonth.getMonth() + 1, 0, 23, 59, 59); // 下个月的第一天的前一天,时间设置为 23:59:59
  490. startOfMonth = this.$moment(startOfMonth).format('YYYY-MM-DD')
  491. endOfMonth = this.$moment(endOfMonth).format('YYYY-MM-DD')
  492. // console.log(startOfMonth); // 输出当前月的结束时间
  493. // console.log(endOfMonth); // 输出当前月的结束时间
  494. this.params.startDate = startOfMonth
  495. this.params.endDate = endOfMonth
  496. this.params.year = this.currentYear + ''
  497. this.cycleValue = this.currentMonth + ''
  498. // console.log("当前月" + this.cycleValue);
  499. this.params = { ...this.params, cycleValue: this.cycleValue }
  500. this.getRecords();
  501. },
  502. // 获取表格数据
  503. getRecords() {
  504. this.loading = true
  505. this.filteredData = []
  506. this.tableData = []
  507. let url = `/performance/statistics/reviews/${this.user_info.site_id}`
  508. this.$axiosUser("get", url, this.params).then(res => {
  509. this.tableData = res.data.data.list;
  510. this.filteredData = this.tableData
  511. this.filteredData.forEach(item => {
  512. item.date = this.formatDate(item.startTime) + "至" + this.formatDate(item.endTime)
  513. })
  514. let deptMap = JSON.parse(localStorage.getItem("SET_EMPLOYEE_MAP"));
  515. if (Object.keys(deptMap) && Object.keys(deptMap).length > 0) {
  516. this.filteredData.forEach(item => {
  517. const employeeDetails = deptMap[item.employeeId];
  518. if (employeeDetails) {
  519. item.department = employeeDetails.employee_detail.dept_list;
  520. }
  521. })
  522. }
  523. this.generalizeList[0].val = this.tableData.length // 总人数
  524. this.generalizeList[1].val = this.tableData.filter(item => item.status == 1).length // 已结束人数
  525. this.generalizeList[2].val = this.tableData.filter(item => item.status == 0).length // 进行中人数
  526. this.generalizeList[3].val = this.tableData.filter(item => item.status == 2).length // 面谈中人数
  527. this.applyFilters();
  528. this.loading = false;
  529. })
  530. },
  531. // 获取全局等级设置
  532. async getAllSet() {
  533. let res = await this.$axiosUser('get', 'api/pro/per/user/base_config')
  534. let data = res.data.data;
  535. let levels = data.level_scope.levels;
  536. let gradeLevels = [];
  537. let max = 0;//最大值
  538. let colorList = ['#5F7294', '#08EEA1', '#f56c6c', '#0887EE', '#92EE08', '#f56c6c']
  539. if (levels && levels.length > 0) {
  540. levels.forEach((item, index) => {
  541. var obj;
  542. if (index == 0) {
  543. obj = { name: item.name, max: Number(item.value), min: 0 };
  544. } else {
  545. obj = { name: item.name, max: Number(item.value), min: max };//当不是第一个等级时,最小值为上一个的最大值
  546. }
  547. obj.color = colorList[index]
  548. max = item.value;
  549. gradeLevels.push(obj);
  550. })
  551. this.gradeLevels = gradeLevels
  552. }
  553. },
  554. // 查找分数对应的等级
  555. findGrade(score, gradeLevels) {
  556. for (let i = 0; i < gradeLevels.length; i++) {
  557. if (score && score >= gradeLevels[i].min && score && score <= gradeLevels[i].max) {
  558. return gradeLevels[i].name; // 返回对应的等级描述
  559. } else if (score && score <= gradeLevels[0].min) {
  560. return gradeLevels[0].name; // 返回对应的等级描述
  561. } else if (score && score >= gradeLevels[gradeLevels.length - 1].max) {
  562. return gradeLevels[gradeLevels.length - 1].name; // 返回对应的等级描述
  563. }
  564. }
  565. return "未评分"; // 如果分数不在任何范围内
  566. },
  567. assignLevels(scores, levelConfigs) {
  568. // 降序排序并去重(假设分数不重复,可省略去重)
  569. const sortedScores = [...scores].sort((a, b) => b - a);
  570. const total = sortedScores.length;
  571. if (total === 0) return [];
  572. // 归一化处理比例
  573. const totalRatio = levelConfigs.reduce((sum, cfg) => sum + cfg.ratio, 0);
  574. const normalized = levelConfigs.map(cfg => cfg.ratio / totalRatio);
  575. // 计算每个等级的初始人数
  576. let counts = normalized.map(ratio => Math.floor(total * ratio));
  577. let remainder = total - counts.reduce((sum, c) => sum + c, 0);
  578. // 分配剩余人数,按优先级顺序
  579. let idx = 0;
  580. while (remainder > 0 && idx < counts.length) {
  581. counts[idx]++;
  582. remainder--;
  583. idx++;
  584. }
  585. // 构建结果:按人数切割数组
  586. let start = 0;
  587. return counts.map((count, i) => {
  588. const end = start + count;
  589. const levelScores = sortedScores.slice(start, end);
  590. start = end;
  591. return {
  592. level: levelConfigs[i].level,
  593. scores: levelScores
  594. };
  595. });
  596. },
  597. // 导出表格
  598. exportToExcel(tableName, elementName) {
  599. if(!(this.filteredData && this.filteredData.length > 0)) return
  600. this.downloadLoading = true;
  601. // 如果未传入文件名,则使用当前时间戳
  602. if (!tableName) {
  603. tableName = new Date().getTime();
  604. }
  605. // 克隆表格 DOM,避免影响原表格
  606. const tableDom = document.querySelector(elementName).cloneNode(true);
  607. const tableHeader = tableDom.querySelector('.el-table__header-wrapper');
  608. const tableBody = tableDom.querySelector('.el-table__body');
  609. tableHeader.childNodes[0].append(tableBody.childNodes[1]);
  610. // 获取表头 DOM
  611. const headerDom = tableHeader.childNodes[0].querySelectorAll('th');
  612. // 移除复选框列
  613. if (headerDom[0].querySelector('.el-checkbox')) {
  614. headerDom[0].remove();
  615. }
  616. // 移除操作列
  617. for (let key in headerDom) {
  618. if (headerDom[key].innerText === '操作') {
  619. headerDom[key].remove();
  620. }
  621. }
  622. // 清理表格中的复选框和按钮
  623. const tableList = tableHeader.childNodes[0].childNodes[2].querySelectorAll('td');
  624. for (let key = 0; key < tableList.length; key++) {
  625. if (tableList[key].querySelector('.el-checkbox') || tableList[key].querySelector('.el-button')) {
  626. tableList[key].remove();
  627. }
  628. }
  629. // 使用 XLSX 将 DOM 转换为 Excel 文件
  630. const webBook = XLSX.utils.table_to_book(tableHeader);
  631. const webOut = XLSX.write(webBook, { bookType: 'xlsx', bookSST: true, type: 'array' });
  632. try {
  633. FileSaver.saveAs(new Blob([webOut], { type: 'application/octet-stream' }), `${tableName}.xlsx`);
  634. } catch (e) {
  635. console.error(e, webOut);
  636. }
  637. this.downloadLoading = false;
  638. },
  639. closeTargetList() {
  640. this.targetDialogVisible = false
  641. },
  642. // 打开OKR弹框
  643. openTargetList(okrs) {
  644. if (okrs && okrs.length > 0) {
  645. this.okrs = okrs
  646. this.targetDialogVisible = true
  647. } else {
  648. return this.$message.error("暂无关联okr")
  649. }
  650. },
  651. // 关闭绩效弹框回调事件
  652. handleClose() {}
  653. }
  654. };
  655. </script>
  656. <style scoped="scoped" lang="scss">
  657. .cursor {
  658. cursor: hand;
  659. &:hover {
  660. cursor: hand; /* 鼠标悬停时改为手型指针 */
  661. }
  662. }
  663. /* 默认鼠标样式为箭头指针 */
  664. .record-container {
  665. width: 100%;
  666. height: 100%;
  667. border-radius: 5px;
  668. .main-content {
  669. width: 100%;
  670. height: 100%;
  671. display: flex;
  672. flex-direction: column;
  673. background: #fff;
  674. padding: 0 10px;
  675. box-sizing: border-box;
  676. .search-box {
  677. display: flex;
  678. align-items: center;
  679. justify-content: space-between;
  680. width: 100%;
  681. height: 50px;
  682. box-sizing: border-box;
  683. background: #fff;
  684. .dept_wdiv {
  685. width: 240px;
  686. position: relative;
  687. .dept_inp {
  688. width: 240px;
  689. z-index: 9;
  690. height: 36px;
  691. line-height: 36px;
  692. border: 1px solid #e0e0e0;
  693. border-radius: 3px;
  694. font-size: 12px;
  695. padding: 0 10px;
  696. overflow: hidden;
  697. white-space: nowrap;
  698. text-overflow: ellipsis;
  699. cursor: pointer;
  700. color: #676767;
  701. }
  702. i {
  703. position: absolute;
  704. top: 0;
  705. right: 0;
  706. top: 12px;
  707. right: 10px;
  708. font-size: 14px;
  709. color: #b5b5b5;
  710. }
  711. }
  712. }
  713. .line {
  714. width: 100%;
  715. height: 1px;
  716. background: #f1f1f1;
  717. }
  718. .generalize-item {
  719. width: 120px;
  720. margin-bottom: 20px;
  721. }
  722. .table-box {
  723. flex: 1;
  724. width: 100%;
  725. overflow-y: auto;
  726. /* 设置滚动条的宽度和背景色 */
  727. &::-webkit-scrollbar {
  728. width: 6px;
  729. height: 6px;
  730. background-color: #f9f9f9;
  731. }
  732. /* 设置滚动条滑块的样式 */
  733. &::-webkit-scrollbar-thumb {
  734. border-radius: 6px;
  735. background-color: #c1c1c1;
  736. }
  737. /* 设置滚动条滑块hover样式 */
  738. &::-webkit-scrollbar-thumb:hover {
  739. background-color: #a8a8a8;
  740. }
  741. /* 设置滚动条轨道的样式 */
  742. &::-webkit-scrollbar-track {
  743. box-shadow: inset 0 0 5px rgba(87, 175, 187, 0.1);
  744. border-radius: 6px;
  745. background: #ededed;
  746. }
  747. }
  748. }
  749. }
  750. </style>