OaCalendar.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <template>
  2. <div class="OaCalendar">
  3. <el-calendar ref="calendar" v-model="todayValue">
  4. <template #header="{ date }">
  5. <div class="selectBox">
  6. <el-icon class="icon1" @click="selectDate('prev-month')"><ArrowLeftBold /></el-icon>
  7. <p>{{ date }}</p>
  8. <el-icon class="icon1" @click="selectDate('next-month')"><ArrowRightBold /></el-icon>
  9. </div>
  10. </template>
  11. <template #date-cell="{ data }">
  12. <!-- 仅工作日显示角标 -->
  13. <div
  14. :class="`day-cell ${getDayClass(data.day)}`"
  15. v-if="data.type === 'current-month'"
  16. @mouseover="onMouseOver(data)"
  17. @mouseout="onMouseOut(data)"
  18. >
  19. <p :class="data.isSelected ? 'is-selected' : ''">
  20. <span v-if="getDayClass(data.day) == 'square-leave'" class="glary">{{
  21. dayReportMap[data.day]?.show
  22. }}</span>
  23. <span v-else>{{ dayReportMap[data.day]?.show }}</span>
  24. </p>
  25. </div>
  26. <div v-else>
  27. <div class="hide-day"> </div>
  28. </div>
  29. <div class="icon" v-if="data.type === 'current-month'">
  30. <el-icon v-if="getDayClass(data.day) == 'square-filled'">
  31. <Select />
  32. </el-icon>
  33. <el-icon v-if="getDayClass(data.day) == 'square-unfilled'"><CloseBold /></el-icon>
  34. <el-icon v-if="getDayClass(data.day) == 'square-today'"><EditPen /></el-icon>
  35. <el-icon v-if="getDayClass(data.day) == 'square-future'"><MoreFilled /></el-icon>
  36. <span v-if="getDayClass(data.day) == 'square-leave'">假</span>
  37. </div>
  38. </template>
  39. </el-calendar>
  40. </div>
  41. </template>
  42. <script setup lang="ts">
  43. import request from '@/config/axios'
  44. import moment from 'moment'
  45. import { getUserInfo } from '@/utils/tool'
  46. import PubsubService from '@/utils/PubsubService'
  47. const message = useMessage()
  48. // 获取用户信息
  49. const userInfo = getUserInfo()
  50. const $emit = defineEmits<{
  51. (e: 'click', v: string | object): void
  52. }>()
  53. const calendar = ref<any>(null)
  54. const todayValue = ref(new Date())
  55. watch(todayValue, () => {
  56. // 1、如果请假,则不进行跳转
  57. const today = moment(todayValue.value).format('YYYY-MM-DD')
  58. const { isworkday = 0, state = '' } = dayReportMap.value[today] ?? {}
  59. // 2、如果选中的是今天之后的日期,则不进行点击及跳转
  60. const isAfterToday = moment(todayValue.value).isAfter(moment())
  61. if (isAfterToday) {
  62. todayValue.value = new Date()
  63. message.info('仅可对今日及之前的日志进行填报!')
  64. } else if (isworkday && leaveList.includes(state)) {
  65. todayValue.value = new Date()
  66. message.info('请假当天日志不需要填报!')
  67. } else {
  68. $emit('click', dayReportMap.value[moment(todayValue.value).format('YYYY-MM-DD')])
  69. }
  70. })
  71. // 切换月份
  72. const selectDate = (val) => {
  73. if (!calendar.value) return
  74. calendar.value.selectDate(val)
  75. if (val === 'prev-month') {
  76. nowMonths.value = moment(nowMonths.value).subtract(1, 'months').format('YYYY-MM')
  77. } else if (val === 'next-month') {
  78. nowMonths.value = moment(nowMonths.value).add(1, 'months').format('YYYY-MM')
  79. }
  80. const timer = setTimeout(() => {
  81. initDailyReportData()
  82. clearTimeout(timer)
  83. }, 128)
  84. }
  85. onMounted(() => {
  86. initDailyReportData().then(() => {
  87. // 为了方便获取提交的内容,需要重置一下日期这些内容
  88. $emit('click', dayReportMap.value[moment(todayValue.value).format('YYYY-MM-DD')])
  89. })
  90. // 订阅提交(暂存)事件,执行重置事件
  91. PubsubService.subscribe('sendReportHandle-init', () => {
  92. initDailyReportData().then(() => {
  93. // 提交后触发点击事件刷新右边的显示内容
  94. $emit('click', dayReportMap.value[moment(todayValue.value).format('YYYY-MM-DD')])
  95. })
  96. })
  97. })
  98. interface ItemState {
  99. id?: string // ID
  100. date: string // 日期
  101. year: string // 年份
  102. month: string // 月份
  103. week: string // 第几周
  104. dayOfWeek: string // 第几天
  105. isworkday: number //是否工作日
  106. state: string //leaveMap
  107. }
  108. // const weekNum = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
  109. // 考勤状态对应字典值 2.26
  110. const leaveMap = {
  111. 1: '出勤',
  112. 2: '迟到',
  113. 3: '早退',
  114. 4: '旷工',
  115. 5: '出差',
  116. 11: '事假',
  117. 12: '病假',
  118. 13: '婚假',
  119. 14: '产假',
  120. 15: '陪产假',
  121. 16: '工伤',
  122. 17: '年假',
  123. 18: '调休',
  124. 19: '丧假',
  125. 20: '其他'
  126. }
  127. // 所有请假的代码,参考leaveMap
  128. const leaveList = ['5', '11', '12', '13', '14', '15', '16', '17', '18', '19']
  129. const dailyReportAttendances = ref<ItemState[]>([])
  130. const nowMonths: any = ref(moment().format('YYYY-MM'))
  131. // const dictMap: any = getDictOptions(DICT_TYPE.ADM_ATTENDANCE_STATUS)
  132. const getWorkDayList = async (): Promise<any> => {
  133. const nowTimeObj = moment(nowMonths.value)
  134. const urlApi = `/adm/workday/list`
  135. const params = {
  136. dateDay: [
  137. nowTimeObj.startOf('months').format('YYYY-MM-DD HH:mm:ss'),
  138. nowTimeObj.endOf('months').format('YYYY-MM-DD HH:mm:ss')
  139. ]
  140. }
  141. return await request.get({ url: urlApi, params })
  142. }
  143. const getAttendanceSheetListMine = async (): Promise<any> => {
  144. const nowTimeObj = moment(nowMonths.value)
  145. const urlApi = `/adm/attendance-sheet/list-me`
  146. const params = {
  147. attendanceTime: [
  148. nowTimeObj.startOf('months').format('YYYY-MM-DD HH:mm:ss'),
  149. nowTimeObj.endOf('months').format('YYYY-MM-DD HH:mm:ss')
  150. ]
  151. }
  152. return await request.get({ url: urlApi, params })
  153. }
  154. // 日报统计
  155. const getLogList = async (): Promise<any> => {
  156. const nowTimeObj = moment(nowMonths.value)
  157. const urlApi = `/adm/report/list`
  158. const params = {
  159. reportType: 'daily',
  160. reportYear: nowTimeObj.format('YYYY'),
  161. reportMonth: Number(nowTimeObj.format('MM')),
  162. userId: userInfo.id ?? ''
  163. }
  164. return await request.get({ url: urlApi, params })
  165. }
  166. const dayReportMap: any = ref({})
  167. const initDailyReportData = async (): Promise<any> => {
  168. // 获取已填写的日志
  169. const logList = await getLogList()
  170. const logObj = {}
  171. logList?.forEach((item) => {
  172. logObj[moment(item.reportStartDate).format('YYYY-MM-DD')] = item
  173. })
  174. //考勤统计
  175. const attendances: [] = await getAttendanceSheetListMine()
  176. const attendanceState: Map<string, string> = new Map()
  177. attendances.forEach((item) => {
  178. if (leaveMap[item['attendanceStatus']]) {
  179. attendanceState[moment(item['attendanceTime']).format('YYYY-MM-DD')] =
  180. item['attendanceStatus']
  181. }
  182. })
  183. //节假日统计
  184. const workdays: any[] = await getWorkDayList()
  185. const dailyReportList: any[] = []
  186. workdays.forEach((item) => {
  187. const date = moment(item['dateDay']).format('YYYY-MM-DD')
  188. const _dailyReportMap = dailyReportList[date]
  189. const _attendanceState = attendanceState[date]
  190. const isLog = logObj[date] ?? false
  191. if (_dailyReportMap) {
  192. dailyReportList.push({
  193. id: _dailyReportMap['id'],
  194. date: moment(item['dateDay']).format('YYYY-MM-DD'),
  195. year: item['year'],
  196. month: item['month'],
  197. week: item['week'],
  198. dayOfWeek: item['dayOfWeek'],
  199. isworkday: item['isworkday'],
  200. state: _dailyReportMap['state'],
  201. isLog,
  202. holidayRemark: item['holidayRemark']
  203. })
  204. } else if (_attendanceState) {
  205. dailyReportList.push({
  206. id: '',
  207. date: moment(item['dateDay']).format('YYYY-MM-DD'),
  208. year: item['year'],
  209. month: item['month'],
  210. week: item['week'],
  211. dayOfWeek: item['dayOfWeek'],
  212. isworkday: item['isworkday'],
  213. state: _attendanceState,
  214. isLog,
  215. holidayRemark: item['holidayRemark']
  216. })
  217. } else {
  218. dailyReportList.push({
  219. id: '',
  220. date: moment(item['dateDay']).format('YYYY-MM-DD'),
  221. year: item['year'],
  222. month: item['month'],
  223. week: item['week'],
  224. dayOfWeek: item['dayOfWeek'],
  225. isworkday: item['isworkday'],
  226. state: item['isworkday'] === 0 ? '' : '-1',
  227. isLog,
  228. holidayRemark: item['holidayRemark']
  229. })
  230. }
  231. })
  232. dailyReportAttendances.value = dailyReportList
  233. dailyReportList.forEach((item) => {
  234. dayReportMap.value[item.date] = { ...item, show: item.date.split('-')[2] }
  235. })
  236. }
  237. /**
  238. * 日报样式设置
  239. * 1、已填,绿色三角+对勾 square-filled
  240. * isworkday == 1, isLog
  241. * 2、未填,红色三角+错号 square-unfilled
  242. * isworkday == 0, isLog == false
  243. * 3、当天,蓝色三角+画笔 square-today
  244. * 4、未到,灰色三角+省略 square-future
  245. * 5、请假,橙色三角+假 square-leave
  246. * 6、暂存,同 未填
  247. */
  248. const getDayClass = (data) => {
  249. const { isworkday = 0, state = '', date = '', isLog } = dayReportMap.value[data] ?? {}
  250. // 是否暂存
  251. const isTemp = isLog?.isTemp ?? false
  252. const isFuture = moment(date).isAfter(moment())
  253. if (isTemp) {
  254. return 'square-unfilled'
  255. }
  256. if (date == moment().format('YYYY-MM-DD')) {
  257. return `square-today`
  258. }
  259. if (isworkday && isLog && !isFuture) {
  260. return `square-filled`
  261. } else if (isworkday && leaveList.includes(state) && !isFuture) {
  262. return `square-leave`
  263. } else if (isworkday && !isLog && !isFuture) {
  264. return `square-unfilled`
  265. } else if (isworkday && isFuture) {
  266. return `square-future`
  267. }
  268. return ''
  269. }
  270. // 鼠标移入移出事件
  271. const onMouseOver = (data) => {
  272. const isLog = dayReportMap.value[data.day]?.isLog ? true : false
  273. if (isLog) {
  274. dayReportMap.value[data.day].show = '已填'
  275. } else {
  276. dayReportMap.value[data.day].show = dayReportMap.value[data.day]?.holidayRemark ?? '未填'
  277. }
  278. }
  279. const onMouseOut = (data) => {
  280. dayReportMap.value[data.day].show = data.day.split('-')[2]
  281. }
  282. </script>
  283. <style lang="scss" scoped>
  284. .OaCalendar {
  285. width: 100%;
  286. :deep(.el-calendar) {
  287. .el-calendar__header {
  288. padding: 0 !important;
  289. border-bottom: none !important;
  290. }
  291. .el-calendar-table {
  292. border: 1px solid #e1e3ea !important;
  293. thead {
  294. background: #f8faff !important;
  295. }
  296. }
  297. .el-calendar-day {
  298. padding: 0;
  299. position: relative;
  300. .icon {
  301. width: 20px;
  302. height: 20px;
  303. text-align: center;
  304. position: absolute;
  305. top: 2px;
  306. right: -1px;
  307. color: #fff;
  308. font-size: 12px;
  309. font-weight: bold;
  310. z-index: 999;
  311. }
  312. }
  313. .day-cell {
  314. width: 100%;
  315. height: 100%;
  316. display: flex;
  317. justify-content: center;
  318. align-items: center;
  319. font-size: 16px;
  320. font-weight: 600;
  321. font-family: DINCond-Bold, DINCond-Bold;
  322. ::before {
  323. content: '';
  324. width: 0;
  325. height: 0;
  326. border-left: 30px solid transparent;
  327. border-bottom: 30px solid transparent;
  328. position: absolute;
  329. top: 0;
  330. right: 0;
  331. }
  332. .glary {
  333. color: #ccc;
  334. }
  335. }
  336. .square-filled {
  337. ::before {
  338. border-right: 30px solid #05ce9e;
  339. }
  340. }
  341. .square-unfilled {
  342. ::before {
  343. border-right: 30px solid #fc4308;
  344. }
  345. }
  346. .square-today {
  347. ::before {
  348. border-right: 30px solid #1b80eb;
  349. }
  350. }
  351. .square-future {
  352. ::before {
  353. border-right: 30px solid #c9cdd8;
  354. }
  355. }
  356. .square-leave {
  357. ::before {
  358. border-right: 30px solid #f1a256;
  359. }
  360. }
  361. .is-selected {
  362. color: #1989fa;
  363. }
  364. }
  365. .selectBox {
  366. display: flex;
  367. align-items: center;
  368. margin-bottom: 10px;
  369. .icon1 {
  370. cursor: pointer;
  371. }
  372. p {
  373. font-weight: 600;
  374. color: #000000;
  375. margin: 0 10px;
  376. user-select: none;
  377. cursor: pointer;
  378. }
  379. }
  380. .contentBox {
  381. width: 100%;
  382. height: 80px;
  383. display: flex;
  384. align-items: center;
  385. justify-content: space-between;
  386. border: 1px solid #dee0e3;
  387. .ulBox {
  388. width: calc(100% - 120px);
  389. height: 100%;
  390. ul {
  391. width: 100%;
  392. height: 100%;
  393. display: flex;
  394. align-items: center;
  395. justify-content: space-between;
  396. }
  397. li {
  398. height: 100%;
  399. > div {
  400. &:first-child {
  401. width: 100%;
  402. height: 50%;
  403. border: 1px solid #dee0e3;
  404. display: flex;
  405. align-items: center;
  406. justify-content: center;
  407. border-right: 0;
  408. border-top: 0;
  409. background-color: #f7f8fa;
  410. &.isToday {
  411. background-color: #2e77e6;
  412. > p {
  413. color: #fff;
  414. }
  415. }
  416. }
  417. }
  418. .topBox {
  419. p {
  420. color: #121518;
  421. user-select: none;
  422. }
  423. }
  424. .topBoxPa {
  425. p {
  426. color: #b9c3c9;
  427. }
  428. }
  429. .topBox:hover {
  430. p {
  431. color: #1b80eb;
  432. }
  433. }
  434. .bomBox {
  435. width: 100%;
  436. height: 50%;
  437. display: flex;
  438. align-items: center;
  439. justify-content: center;
  440. &.imgHover_-1,
  441. &.imgHover_99 {
  442. cursor: pointer;
  443. }
  444. &.imgHover_-1:hover {
  445. position: relative;
  446. &::after {
  447. position: absolute;
  448. display: block;
  449. content: '补';
  450. width: 100%;
  451. height: 100%;
  452. background-color: #fff;
  453. top: 0px;
  454. left: 0px;
  455. line-height: 40px;
  456. text-align: center;
  457. color: #999;
  458. }
  459. }
  460. &.imgHover {
  461. &:hover {
  462. border: 1px solid #f00;
  463. }
  464. }
  465. img {
  466. user-select: none;
  467. }
  468. > span {
  469. cursor: pointer;
  470. &.leave {
  471. color: #f00;
  472. cursor: default;
  473. }
  474. }
  475. }
  476. }
  477. li:first-child {
  478. .topBox {
  479. border-left: 0;
  480. }
  481. }
  482. li:last-child {
  483. .topBox {
  484. border-right: 0;
  485. }
  486. }
  487. }
  488. .infoBox {
  489. width: 120px;
  490. height: 100%;
  491. display: flex;
  492. align-items: center;
  493. justify-content: space-between;
  494. flex-wrap: wrap;
  495. background-color: #edf0f4;
  496. border-left: 1px solid #dee0e3;
  497. span {
  498. display: block;
  499. width: 100%;
  500. text-align: center;
  501. font-size: 14px;
  502. }
  503. p {
  504. width: 100%;
  505. text-align: center;
  506. font-size: 18px;
  507. }
  508. }
  509. }
  510. }
  511. </style>