dep.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. <template>
  2. <div class="attendanceCenterDep">
  3. <h1>考勤统计表</h1>
  4. <div class="depSearch">
  5. <div class="depSearchFlex">
  6. <div class="searBox">
  7. <span class="span">月份:</span>
  8. <el-date-picker v-model="fromParams.month" type="month" placeholder="请选择月份" />
  9. </div>
  10. <div class="searBox" v-if="porps.deptsName == '公司'">
  11. <span class="span">部门:</span>
  12. <DeptTree
  13. v-model="fromParams['deptId']"
  14. placeholder="请选择部门"
  15. :disabled="porps.deptsName !== '公司'"
  16. check-strictly
  17. />
  18. </div>
  19. <div class="searBox">
  20. <span class="span">人员:</span>
  21. <el-input v-model="fromParams.userName" placeholder="请输入人员名称" />
  22. </div>
  23. <div class="searBtns">
  24. <el-button @click="querysClick" type="primary" style="background: #3485ff">
  25. <img src="@/assets/imgs/OA/search.png" class="mr-8px" alt="" />
  26. 查询</el-button
  27. >
  28. <ExportToExcel
  29. v-if="porps.deptsName == '公司'"
  30. :data="excelDataSource"
  31. :file-name="`${moment(fromParams.month).format('YYYY年MM月')}考勤统计.xlsx`"
  32. :colsWidth="colsWidth"
  33. :title="`${moment(fromParams.month).format('YYYY年MM月')}-考勤数据表`"
  34. />
  35. <el-button
  36. v-if="porps.deptsName == '公司'"
  37. type="warning"
  38. plain
  39. @click="handleImport"
  40. v-hasPermi="['adm:attendance-sheet:import']"
  41. >
  42. <Icon icon="ep:upload" /> 导入
  43. </el-button>
  44. <!-- <el-button v-if="porps.deptsName == '公司'" plain @click="dialogVisible = true">
  45. 免考勤人员设置
  46. </el-button> -->
  47. <el-button type="success" style="background-color: #05ce9e" v-if="false">
  48. <img src="@/assets/imgs/OA/kq/kqqrIcon.png" class="mr-8px" alt="" />
  49. 考勤确认</el-button
  50. >
  51. </div>
  52. </div>
  53. <!-- 免考勤人员设置 -->
  54. <el-dialog v-model="dialogVisible" title="免考勤人员设置" width="500" align-center>
  55. <UserOrgTree v-model="notAttendanceList" :multiple="true" placeholder="请选择免考勤人员" />
  56. <template #footer>
  57. <div class="dialog-footer">
  58. <el-button @click="dialogVisible = false">关闭</el-button>
  59. <el-button type="primary" @click="noAttendanceSet"> 确定 </el-button>
  60. </div>
  61. </template>
  62. </el-dialog>
  63. <el-popover :width="50">
  64. <template #reference>
  65. <div class="depStateBox">
  66. <p>?</p>
  67. </div>
  68. </template>
  69. <template #default>
  70. <div class="depStateContent">
  71. <ul class="">
  72. <li class="flex" v-for="(l, i) in depStateContentList" :key="i">
  73. <div v-if="i == 0" class="flex">
  74. <img src="@/assets/imgs/OA/kq/gouzi.png" alt="" />:
  75. <p>{{ l.name }}</p>
  76. </div>
  77. <div v-else class="flex">
  78. <h4>{{ l.deps }}:</h4>
  79. <p>{{ l.name }}</p>
  80. </div>
  81. </li>
  82. </ul>
  83. </div>
  84. </template>
  85. </el-popover>
  86. </div>
  87. <div class="depTable">
  88. <el-table
  89. v-loading="tableLoading"
  90. :data="
  91. tableData.slice(
  92. (pagesList.pageNo - 1) * pagesList.pageSize,
  93. pagesList.pageSize * pagesList.pageNo
  94. )
  95. "
  96. style="width: 100%; height: 100%"
  97. stripe
  98. >
  99. <!-- <el-table-column type="index" width="50" /> -->
  100. <el-table-column prop="deptName" label="部门" />
  101. <el-table-column prop="nickName" label="姓名" width="80" />
  102. <el-table-column
  103. v-for="(item, index) in tableHeadList"
  104. :key="index"
  105. :label="item.dayOfWeek"
  106. width="30"
  107. :label-class-name="item.isworkday == 1 ? '' : 'workday'"
  108. >
  109. <el-table-column
  110. :label-class-name="item.isworkday == 1 ? '' : 'workday'"
  111. :label="item.day"
  112. width="30"
  113. >
  114. <template #default="scope">
  115. <div class="iconsFlex" v-if="item.isworkday == 1">
  116. <div class="sw" @click="swStateClick(scope.row, item, 1)">
  117. <div v-if="sco(scope.row, item, 1) == '1'">
  118. <img src="@/assets/imgs/OA/kq/gouzi.png" alt="" />
  119. </div>
  120. <div v-else class="spans">
  121. {{ attendanceTypeObj[sco(scope.row, item, 1)] ?? '' }}
  122. </div>
  123. </div>
  124. <div class="xw" @click="swStateClick(scope.row, item, 2)">
  125. <div v-if="sco(scope.row, item, 2) == '1'">
  126. <img src="@/assets/imgs/OA/kq/gouzi.png" alt="" />
  127. </div>
  128. <div v-else class="spans">
  129. {{ attendanceTypeObj[sco(scope.row, item, 2)] ?? '' }}
  130. </div>
  131. </div>
  132. </div>
  133. </template>
  134. </el-table-column>
  135. </el-table-column>
  136. <el-table-column
  137. v-for="(item, index) in columList"
  138. :prop="item.prop"
  139. width="40"
  140. label-class-name="labelModeRl"
  141. :key="index"
  142. >
  143. <template #header>
  144. <span>{{ item.text }}</span>
  145. <span class="m-t-8px font-size-14px c-#989FA8">{{ item.unit }}</span>
  146. </template>
  147. </el-table-column>
  148. </el-table>
  149. </div>
  150. <div class="depPages">
  151. <el-pagination
  152. :current-page="pagesList.pageNo"
  153. :page-size="pagesList.pageSize"
  154. background
  155. layout="total, prev, pager, next, jumper"
  156. :total="pagesList.total"
  157. @current-change="handleCurrentChange"
  158. />
  159. </div>
  160. <DepsUpdataDetail ref="depsDetailRef" @updataInit="updataInit" />
  161. </div>
  162. <!-- 用户导入对话框 -->
  163. <AttendanceSheetImportForm ref="importFormRef" @success="init" />
  164. </template>
  165. <script setup lang="ts">
  166. defineOptions({ name: 'AttendanceDep' })
  167. import * as DeptApi from '@/api/system/dept'
  168. import DepsUpdataDetail from './components/depsUpdataDetail.vue'
  169. import { handleTree } from '@/utils/tree'
  170. import * as MineApi from '@/api/oa/attendanceCenter'
  171. import { arrFlatten, depSort, isArrayDelOrNickname, allDeptsArr, dayOfWeekCall } from './attendAuth'
  172. import moment from 'moment'
  173. import { depsState } from './depsState'
  174. import ExportToExcel from '@/components/ExportToExcel/index.vue'
  175. import { getUserInfo } from '@/utils/tool'
  176. import AttendanceSheetImportForm from './components/attendanceSheetImportForm.vue'
  177. import DeptTree from '@/views/OaSystem/components/DeptTree/index.vue'
  178. import UserOrgTree from '@/views/OaSystem/components/UserOrgTree/index.vue'
  179. const userInfo = getUserInfo()
  180. const porps = defineProps(['deptsName'])
  181. const pagesList: any = ref({
  182. pageNo: 1,
  183. pageSize: 12,
  184. total: 0
  185. })
  186. const fromParams: any = ref({
  187. deptId: userInfo.deptId ?? '',
  188. month: '',
  189. userName: ''
  190. })
  191. if (porps.deptsName == '公司') {
  192. fromParams.value.deptId = '4e99393c-c0ea-4146-a7fb-56fb8019c477'
  193. }
  194. const deptSort: any = ref([])
  195. const deptList = ref<Tree[]>([]) // 树形结构
  196. const tableData = ref([])
  197. const initTreeDeps = async () => {
  198. DeptApi.getSimpleDeptList().then((res) => {
  199. deptList.value = handleTree(res)
  200. deptSort.value = arrFlatten(res, 'children')
  201. })
  202. }
  203. const depStateContentList = ref()
  204. depStateContentList.value = depsState
  205. const handleCurrentChange = async (page) => {
  206. pagesList.value.pageNo = page
  207. let toMoseMonths: any = []
  208. toMoseMonths[0] = moment().startOf('months').format('YYYY-MM-DD') + ' 00:00:00'
  209. toMoseMonths[1] = moment().endOf('months').format('YYYY-MM-DD') + ' 23:59:59'
  210. // initInsMouth(toMoseMonths)
  211. }
  212. const querysClick = async () => {
  213. // initInsMouth()
  214. pagesList.value.pageNo = 1
  215. let month = fromParams.value.month
  216. let toMoseMonths: any = []
  217. toMoseMonths[0] = moment(month).startOf('months').format('YYYY-MM-DD') + ' 00:00:00'
  218. toMoseMonths[1] = moment(month).endOf('months').format('YYYY-MM-DD') + ' 23:59:59'
  219. initInsMouth(toMoseMonths)
  220. }
  221. const tableLoading = ref(true)
  222. const updataInit = (date) => {
  223. initInsMouth(date)
  224. }
  225. const initInsMouth = async (date: any) => {
  226. tableLoading.value = true
  227. let params = {
  228. attendanceDate: date, // 考勤时间
  229. attendanceStatus: '', // 考勤状态,示例值(2)
  230. attendanceType: '', // 考勤类型(1:上午;2:下午),示例值(1)
  231. otherMinute: '', // 其他考勤状态分钟(除了出勤)
  232. nickname: fromParams.value.userName, //用户昵称
  233. deptId: fromParams.value.deptId //部门ID
  234. }
  235. let urlApi = initWorkDay(date).then((restall) => {
  236. if (porps.deptsName == '公司') {
  237. MineApi.getWorkdaySheetList(params).then((res) => {
  238. let namesArr: any = isArrayDelOrNickname(JSON.parse(JSON.stringify(res)))
  239. let resArr: any = res
  240. resArr.forEach((item: any) => {
  241. if (item.attendanceTime) {
  242. item.date = moment(item.attendanceTime).format('YYYY-MM-DD')
  243. } else {
  244. item.date = moment(item.attendanceDate).format('YYYY-MM-DD')
  245. }
  246. })
  247. let arr = allDeptsArr(restall, resArr, namesArr)
  248. let sbArr = depSort(deptSort.value, arr)
  249. // 导出数据
  250. excelDataSource.value = getExcelTable(sbArr)
  251. tableData.value = sbArr
  252. pagesList.value.total = arr.length
  253. tableLoading.value = false
  254. })
  255. } else {
  256. MineApi.getWorkdaySheetDeptList(params).then((res) => {
  257. let namesArr: any = isArrayDelOrNickname(JSON.parse(JSON.stringify(res)))
  258. let resArr: any = res
  259. resArr.forEach((item: any) => {
  260. if (item.attendanceTime) {
  261. item.date = moment(item.attendanceTime).format('YYYY-MM-DD')
  262. } else {
  263. item.date = moment(item.attendanceDate).format('YYYY-MM-DD')
  264. }
  265. })
  266. let arr = allDeptsArr(restall, resArr, namesArr)
  267. let sbArr = depSort(deptSort.value, arr)
  268. // 导出数据
  269. excelDataSource.value = getExcelTable(sbArr)
  270. tableData.value = sbArr
  271. pagesList.value.total = arr.length
  272. tableLoading.value = false
  273. })
  274. }
  275. })
  276. }
  277. const sco = (row, item, index) => {
  278. let kq = ''
  279. if (index == 1) {
  280. row.attendArray.forEach((k) => {
  281. if (k.date == item.date) {
  282. kq = k.swAttendanceStatus ?? ''
  283. }
  284. })
  285. } else {
  286. row.attendArray.forEach((k) => {
  287. if (k.date == item.date) {
  288. kq = k.xwAttendanceStatus ?? ''
  289. }
  290. })
  291. }
  292. return kq
  293. }
  294. const tableHeadList: any = ref([])
  295. const initWorkDay = async (date: any) => {
  296. let params = {
  297. dateDay: date, // 日期(数组)
  298. dayOfWeek: '', // 星期几
  299. holidayRemark: '', // 节假备注,示例值(国庆节)
  300. isworkday: '', // 是否工作日
  301. year: '', // 年份
  302. month: '', // 月份
  303. week: '' // 周
  304. }
  305. const res = await MineApi.getWorkdayList(params)
  306. let arr = res
  307. arr.forEach((item: any) => {
  308. item.date = moment(item.dateDay).format('YYYY-MM-DD')
  309. item.prop = item.dayOfWeek
  310. item.dayOfWeek = dayOfWeekCall(item.dayOfWeek)
  311. item.day = moment(item.dateDay).format('DD')
  312. })
  313. tableHeadList.value = arr
  314. return await arr
  315. }
  316. const depsDetailRef = ref()
  317. const swStateClick = (row, item, index) => {
  318. //更改考勤
  319. if (porps.deptsName == '公司') {
  320. depsDetailRef.value.initShow(row, item, index, fromParams.value.month)
  321. return
  322. }
  323. }
  324. // 考勤对应的字典
  325. const attendanceTypeObj = {
  326. '1': '', // 出勤
  327. '2': '迟', // '迟到'
  328. '3': '早', // '早退'
  329. '4': '旷', // '旷工'
  330. '5': '差', // '出差'
  331. '11': '事', // '事假'
  332. '12': '病', // '病假'
  333. '13': '婚', // '婚假'
  334. '14': '产', // '产假'
  335. '15': '陪', // '陪产假'
  336. '19': '丧', // '丧假'
  337. '16': '伤', // '工伤'
  338. '17': '年', // '年假'
  339. '18': '调', // '调休'
  340. '20': '假' // '其他'
  341. }
  342. // 考勤统计
  343. const columList = [
  344. { text: '旷 工', prop: 'kg', unit: '天' },
  345. { text: '迟 到', prop: 'cd', unit: '次' },
  346. { text: '早 退', prop: 'zt', unit: '次' },
  347. { text: '出 差', prop: 'cc', unit: '天' },
  348. { text: '事 假', prop: 'sj', unit: '天' },
  349. { text: '调 休', prop: 'tx', unit: '天' },
  350. { text: '病 假', prop: 'bj', unit: '天' },
  351. { text: '年 假', prop: 'nj', unit: '天' },
  352. { text: '产 假', prop: 'cj', unit: '天' },
  353. { text: '其 他', prop: 'qt', unit: '天' }
  354. ]
  355. // 考勤表格
  356. const excelDataSource = ref<any>()
  357. // 合并的单元格
  358. // const mergeRanges = [
  359. // { s: { r: 0, c: 0 }, e: { r: 1, c: 0 } }, // 合并第一行的前两列
  360. // { s: { r: 0, c: 1 }, e: { r: 1, c: 1 } } // 合并第二行的前两列
  361. // ]
  362. // 列宽设置
  363. const colsWidth = ref([{ wch: 30 }, { wch: 10 }, { wch: 8 }])
  364. // 获取导出的数据表格
  365. const getExcelTable = (attendanceData) => {
  366. const tableHead = [['部门', '姓名', '时间', ...tableHeadList.value.map((item) => item.day)]]
  367. // 日期单元格列宽设置
  368. colsWidth.value = [...colsWidth.value, ...tableHeadList.value.map(() => ({ wch: 3 }))]
  369. const tableBody: any = []
  370. for (let index = 0; index < attendanceData.length; index++) {
  371. const item = attendanceData[index]
  372. const data1 = item.attendArray.map((arr, i) => {
  373. // 判断是不是工作日
  374. const isworkday = tableHeadList.value[i].isworkday
  375. if (!isworkday) return ''
  376. // 判断有没有数据
  377. if (!arr.swAttendanceStatus) return ''
  378. return attendanceTypeObj[arr.swAttendanceStatus] ?? ''
  379. })
  380. const data2 = item.attendArray.map((arr, i) => {
  381. const isworkday = tableHeadList.value[i].isworkday
  382. if (!isworkday) return ''
  383. if (!arr.swAttendanceStatus) return ''
  384. return attendanceTypeObj[arr.xwAttendanceStatus] ?? ''
  385. })
  386. tableBody.push(
  387. [item.deptName, item.nickName, '上午', ...data1],
  388. [item.deptName, item.nickName, '下午', ...data2]
  389. )
  390. }
  391. return [...tableHead, ...tableBody]
  392. }
  393. /** 考勤导入 */
  394. const importFormRef = ref()
  395. const handleImport = () => {
  396. importFormRef.value.open()
  397. }
  398. /** 初始化 */
  399. const init = () => {
  400. let toMonth = moment().format('YYYY-MM')
  401. fromParams.value.month = toMonth
  402. // 加载部门树
  403. initTreeDeps()
  404. let toMoseMonths: any = []
  405. toMoseMonths[0] = moment().startOf('months').format('YYYY-MM-DD') + ' 00:00:00'
  406. toMoseMonths[1] = moment().endOf('months').format('YYYY-MM-DD') + ' 23:59:59'
  407. initInsMouth(toMoseMonths)
  408. }
  409. onMounted(() => {
  410. init()
  411. })
  412. /** 免考勤人员设置 */
  413. const dialogVisible = ref(false)
  414. const notAttendanceList = ref([])
  415. const noAttendanceSet = () => {
  416. dialogVisible.value = false
  417. // console.log('notAttendanceList', notAttendanceList.value)
  418. }
  419. </script>
  420. <style lang="scss" scoped>
  421. .attendanceCenterDep {
  422. position: relative;
  423. width: 100%;
  424. height: calc(100% - 15px);
  425. padding: 15px 30px;
  426. margin-top: 15px;
  427. background: #fff;
  428. border-radius: 10px;
  429. h1 {
  430. margin-bottom: 20px;
  431. font-size: 20px;
  432. font-weight: 600;
  433. color: #121518;
  434. }
  435. .depSearch {
  436. display: flex;
  437. width: 100%;
  438. margin-bottom: 20px;
  439. align-items: center;
  440. justify-content: space-between;
  441. .depSearchFlex {
  442. display: flex;
  443. align-items: center;
  444. .searBox {
  445. display: flex;
  446. align-items: center;
  447. margin-right: 25px;
  448. .span {
  449. font-size: 16px;
  450. white-space: nowrap;
  451. }
  452. }
  453. }
  454. .depStateBox {
  455. display: flex;
  456. width: 30px;
  457. height: 30px;
  458. cursor: pointer;
  459. border: 2px solid #e1e4ea;
  460. border-radius: 50%;
  461. align-items: center;
  462. justify-content: center;
  463. p {
  464. font-size: 16px;
  465. font-weight: 600;
  466. color: #e1e4ea;
  467. }
  468. }
  469. }
  470. .depTable {
  471. width: 100%;
  472. height: calc(100% - 140px);
  473. }
  474. .depPages {
  475. position: absolute;
  476. right: 30px;
  477. bottom: 10px;
  478. }
  479. }
  480. :deep(.depTable) {
  481. .el-table {
  482. .workDayClass {
  483. color: pink;
  484. }
  485. .is-group {
  486. height: 80px;
  487. tr {
  488. background-color: #e5f0fb;
  489. th {
  490. font-weight: 500;
  491. color: #233755;
  492. background-color: #e5f0fb;
  493. border-color: #fff;
  494. }
  495. }
  496. }
  497. .cell {
  498. width: 100%;
  499. height: 100%;
  500. padding: 0 !important;
  501. font-size: 16px !important;
  502. text-align: center;
  503. }
  504. .el-table__cell {
  505. // padding: 0;
  506. }
  507. .el-table__body-wrapper {
  508. tr {
  509. td {
  510. border: 0;
  511. }
  512. }
  513. .el-table__row--striped {
  514. td {
  515. background-color: #f7f7f7;
  516. }
  517. }
  518. }
  519. .iconsFlex {
  520. display: flex;
  521. width: 100%;
  522. height: 100%;
  523. align-items: center;
  524. flex-wrap: wrap;
  525. .spans {
  526. display: flex;
  527. width: 100%;
  528. height: 24px;
  529. padding: 0;
  530. margin: 0;
  531. font-size: 14px !important;
  532. font-weight: 800;
  533. color: #f23c3c;
  534. cursor: pointer;
  535. user-select: none;
  536. justify-content: center;
  537. align-items: center;
  538. }
  539. .sw {
  540. display: flex;
  541. width: 100%;
  542. height: 50%;
  543. user-select: none;
  544. align-items: center;
  545. justify-content: center;
  546. }
  547. .sw:hover {
  548. .spans {
  549. color: #989fa8;
  550. }
  551. }
  552. .xw:hover {
  553. .spans {
  554. color: #989fa8;
  555. }
  556. }
  557. .xw {
  558. display: flex;
  559. width: 100%;
  560. height: 50%;
  561. user-select: none;
  562. align-items: center;
  563. justify-content: center;
  564. }
  565. }
  566. }
  567. .labelModeRl {
  568. writing-mode: vertical-rl;
  569. }
  570. }
  571. :deep(.workday) {
  572. color: #989fa8 !important;
  573. }
  574. </style>