dep.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  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">
  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. initWorkDay(date).then((restall) => {
  236. MineApi.getWorkdaySheetList(params).then((res) => {
  237. let namesArr: any = isArrayDelOrNickname(JSON.parse(JSON.stringify(res)))
  238. let resArr: any = res
  239. resArr.forEach((item: any) => {
  240. if (item.attendanceTime) {
  241. item.date = moment(item.attendanceTime).format('YYYY-MM-DD')
  242. } else {
  243. item.date = moment(item.attendanceDate).format('YYYY-MM-DD')
  244. }
  245. })
  246. let arr = allDeptsArr(restall, resArr, namesArr)
  247. let sbArr = depSort(deptSort.value, arr)
  248. // 导出数据
  249. excelDataSource.value = getExcelTable(sbArr)
  250. tableData.value = sbArr
  251. pagesList.value.total = arr.length
  252. tableLoading.value = false
  253. })
  254. })
  255. }
  256. const sco = (row, item, index) => {
  257. let kq = ''
  258. if (index == 1) {
  259. row.attendArray.forEach((k) => {
  260. if (k.date == item.date) {
  261. kq = k.swAttendanceStatus ?? ''
  262. }
  263. })
  264. } else {
  265. row.attendArray.forEach((k) => {
  266. if (k.date == item.date) {
  267. kq = k.xwAttendanceStatus ?? ''
  268. }
  269. })
  270. }
  271. return kq
  272. }
  273. const tableHeadList: any = ref([])
  274. const initWorkDay = async (date: any) => {
  275. let params = {
  276. dateDay: date, // 日期(数组)
  277. dayOfWeek: '', // 星期几
  278. holidayRemark: '', // 节假备注,示例值(国庆节)
  279. isworkday: '', // 是否工作日
  280. year: '', // 年份
  281. month: '', // 月份
  282. week: '' // 周
  283. }
  284. const res = await MineApi.getWorkdayList(params)
  285. let arr = res
  286. arr.forEach((item: any) => {
  287. item.date = moment(item.dateDay).format('YYYY-MM-DD')
  288. item.prop = item.dayOfWeek
  289. item.dayOfWeek = dayOfWeekCall(item.dayOfWeek)
  290. item.day = moment(item.dateDay).format('DD')
  291. })
  292. tableHeadList.value = arr
  293. return await arr
  294. }
  295. const depsDetailRef = ref()
  296. const swStateClick = (row, item, index) => {
  297. //更改考勤
  298. if (porps.deptsName == '公司') {
  299. depsDetailRef.value.initShow(row, item, index, fromParams.value.month)
  300. return
  301. }
  302. }
  303. // 考勤对应的字典
  304. const attendanceTypeObj = {
  305. '1': '', // 出勤
  306. '2': '迟', // '迟到'
  307. '3': '早', // '早退'
  308. '4': '旷', // '旷工'
  309. '5': '差', // '出差'
  310. '11': '事', // '事假'
  311. '12': '病', // '病假'
  312. '13': '婚', // '婚假'
  313. '14': '产', // '产假'
  314. '15': '陪', // '陪产假'
  315. '19': '丧', // '丧假'
  316. '16': '伤', // '工伤'
  317. '17': '年', // '年假'
  318. '18': '调', // '调休'
  319. '20': '假' // '其他'
  320. }
  321. // 考勤统计
  322. const columList = [
  323. { text: '旷 工', prop: 'kg', unit: '天' },
  324. { text: '迟 到', prop: 'cd', unit: '次' },
  325. { text: '早 退', prop: 'zt', unit: '次' },
  326. { text: '出 差', prop: 'cc', unit: '天' },
  327. { text: '事 假', prop: 'sj', unit: '天' },
  328. { text: '调 休', prop: 'tx', unit: '天' },
  329. { text: '病 假', prop: 'bj', unit: '天' },
  330. { text: '年 假', prop: 'nj', unit: '天' },
  331. { text: '产 假', prop: 'cj', unit: '天' },
  332. { text: '其 他', prop: 'qt', unit: '天' }
  333. ]
  334. // 考勤表格
  335. const excelDataSource = ref<any>()
  336. // 合并的单元格
  337. // const mergeRanges = [
  338. // { s: { r: 0, c: 0 }, e: { r: 1, c: 0 } }, // 合并第一行的前两列
  339. // { s: { r: 0, c: 1 }, e: { r: 1, c: 1 } } // 合并第二行的前两列
  340. // ]
  341. // 列宽设置
  342. const colsWidth = ref([{ wch: 30 }, { wch: 10 }, { wch: 8 }])
  343. // 获取导出的数据表格
  344. const getExcelTable = (attendanceData) => {
  345. const tableHead = [['部门', '姓名', '时间', ...tableHeadList.value.map((item) => item.day)]]
  346. // 日期单元格列宽设置
  347. colsWidth.value = [...colsWidth.value, ...tableHeadList.value.map(() => ({ wch: 3 }))]
  348. const tableBody: any = []
  349. for (let index = 0; index < attendanceData.length; index++) {
  350. const item = attendanceData[index]
  351. const data1 = item.attendArray.map((arr, i) => {
  352. // 判断是不是工作日
  353. const isworkday = tableHeadList.value[i].isworkday
  354. if (!isworkday) return ''
  355. // 判断有没有数据
  356. if (!arr.swAttendanceStatus) return ''
  357. return attendanceTypeObj[arr.swAttendanceStatus] ?? ''
  358. })
  359. const data2 = item.attendArray.map((arr, i) => {
  360. const isworkday = tableHeadList.value[i].isworkday
  361. if (!isworkday) return ''
  362. if (!arr.swAttendanceStatus) return ''
  363. return attendanceTypeObj[arr.xwAttendanceStatus] ?? ''
  364. })
  365. tableBody.push(
  366. [item.deptName, item.nickName, '上午', ...data1],
  367. [item.deptName, item.nickName, '下午', ...data2]
  368. )
  369. }
  370. return [...tableHead, ...tableBody]
  371. }
  372. /** 考勤导入 */
  373. const importFormRef = ref()
  374. const handleImport = () => {
  375. importFormRef.value.open()
  376. }
  377. /** 初始化 */
  378. const init = () => {
  379. let toMonth = moment().format('YYYY-MM')
  380. fromParams.value.month = toMonth
  381. // 加载部门树
  382. initTreeDeps()
  383. let toMoseMonths: any = []
  384. toMoseMonths[0] = moment().startOf('months').format('YYYY-MM-DD') + ' 00:00:00'
  385. toMoseMonths[1] = moment().endOf('months').format('YYYY-MM-DD') + ' 23:59:59'
  386. initInsMouth(toMoseMonths)
  387. }
  388. onMounted(() => {
  389. init()
  390. })
  391. /** 免考勤人员设置 */
  392. const dialogVisible = ref(false)
  393. const notAttendanceList = ref([])
  394. const noAttendanceSet = () => {
  395. dialogVisible.value = false
  396. // console.log('notAttendanceList', notAttendanceList.value)
  397. }
  398. </script>
  399. <style lang="scss" scoped>
  400. .attendanceCenterDep {
  401. position: relative;
  402. width: 100%;
  403. height: calc(100% - 15px);
  404. padding: 15px 30px;
  405. margin-top: 15px;
  406. background: #fff;
  407. border-radius: 10px;
  408. h1 {
  409. margin-bottom: 20px;
  410. font-size: 20px;
  411. font-weight: 600;
  412. color: #121518;
  413. }
  414. .depSearch {
  415. display: flex;
  416. width: 100%;
  417. margin-bottom: 20px;
  418. align-items: center;
  419. justify-content: space-between;
  420. .depSearchFlex {
  421. display: flex;
  422. align-items: center;
  423. .searBox {
  424. display: flex;
  425. align-items: center;
  426. margin-right: 25px;
  427. .span {
  428. font-size: 16px;
  429. white-space: nowrap;
  430. }
  431. }
  432. }
  433. .depStateBox {
  434. display: flex;
  435. width: 30px;
  436. height: 30px;
  437. cursor: pointer;
  438. border: 2px solid #e1e4ea;
  439. border-radius: 50%;
  440. align-items: center;
  441. justify-content: center;
  442. p {
  443. font-size: 16px;
  444. font-weight: 600;
  445. color: #e1e4ea;
  446. }
  447. }
  448. }
  449. .depTable {
  450. width: 100%;
  451. height: calc(100% - 140px);
  452. }
  453. .depPages {
  454. position: absolute;
  455. right: 30px;
  456. bottom: 10px;
  457. }
  458. }
  459. :deep(.depTable) {
  460. .el-table {
  461. .workDayClass {
  462. color: pink;
  463. }
  464. .is-group {
  465. height: 80px;
  466. tr {
  467. background-color: #e5f0fb;
  468. th {
  469. font-weight: 500;
  470. color: #233755;
  471. background-color: #e5f0fb;
  472. border-color: #fff;
  473. }
  474. }
  475. }
  476. .cell {
  477. width: 100%;
  478. height: 100%;
  479. padding: 0 !important;
  480. font-size: 16px !important;
  481. text-align: center;
  482. }
  483. .el-table__cell {
  484. // padding: 0;
  485. }
  486. .el-table__body-wrapper {
  487. tr {
  488. td {
  489. border: 0;
  490. }
  491. }
  492. .el-table__row--striped {
  493. td {
  494. background-color: #f7f7f7;
  495. }
  496. }
  497. }
  498. .iconsFlex {
  499. display: flex;
  500. width: 100%;
  501. height: 100%;
  502. align-items: center;
  503. flex-wrap: wrap;
  504. .spans {
  505. width: 100%;
  506. height: 24px;
  507. font-size: 14px !important;
  508. font-weight: 800;
  509. color: #f23c3c;
  510. cursor: pointer;
  511. user-select: none;
  512. margin: 0;
  513. padding: 0;
  514. display: flex;
  515. justify-content: center;
  516. align-items: center;
  517. }
  518. .sw {
  519. display: flex;
  520. width: 100%;
  521. height: 50%;
  522. user-select: none;
  523. align-items: center;
  524. justify-content: center;
  525. }
  526. .sw:hover {
  527. .spans {
  528. color: #989fa8;
  529. }
  530. }
  531. .xw:hover {
  532. .spans {
  533. color: #989fa8;
  534. }
  535. }
  536. .xw {
  537. display: flex;
  538. width: 100%;
  539. height: 50%;
  540. user-select: none;
  541. align-items: center;
  542. justify-content: center;
  543. }
  544. }
  545. }
  546. .labelModeRl {
  547. writing-mode: vertical-rl;
  548. }
  549. }
  550. :deep(.workday) {
  551. color: #989fa8 !important;
  552. }
  553. </style>