|
@@ -0,0 +1,466 @@
|
|
|
+<template>
|
|
|
+ <div class="OaCalendar">
|
|
|
+ <el-calendar ref="calendar" :model-value="todayValue">
|
|
|
+ <template #header="{ date }">
|
|
|
+ <div class="selectBox">
|
|
|
+ <p>{{ date }}</p>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template #date-cell="{ data }">
|
|
|
+ <!-- 仅工作日显示角标 -->
|
|
|
+ <div
|
|
|
+ :class="`day-cell ${getDayClass(data.day)}`"
|
|
|
+ v-if="data.type === 'current-month'"
|
|
|
+ @mouseover="onMouseOver(data)"
|
|
|
+ @mouseout="onMouseOut(data)"
|
|
|
+ >
|
|
|
+ <p :class="data.isSelected ? 'is-selected' : ''">
|
|
|
+ {{ dayReportMap[data.day]?.show }}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else>
|
|
|
+ <div class="hide-day"> </div>
|
|
|
+ </div>
|
|
|
+ <div class="icon" v-if="data.type === 'current-month'">
|
|
|
+ <el-icon v-if="getDayClass(data.day) == 'square-filled'">
|
|
|
+ <Select />
|
|
|
+ </el-icon>
|
|
|
+ <el-icon v-if="getDayClass(data.day) == 'square-unfilled'"><CloseBold /></el-icon>
|
|
|
+ <!-- <el-icon v-if="getDayClass(data.day) == 'square-today'"><EditPen /></el-icon> -->
|
|
|
+ <el-icon v-if="getDayClass(data.day) == 'square-future'"><MoreFilled /></el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-calendar>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<script setup lang="ts">
|
|
|
+import request from '@/config/axios'
|
|
|
+import moment from 'moment'
|
|
|
+import { getUserInfo } from '@/utils/tool'
|
|
|
+
|
|
|
+interface IProp {
|
|
|
+ detailTime: any
|
|
|
+}
|
|
|
+const { detailTime } = defineProps<IProp>()
|
|
|
+// 获取用户信息
|
|
|
+const userInfo = getUserInfo()
|
|
|
+
|
|
|
+const calendar = ref()
|
|
|
+
|
|
|
+const todayValue = ref(moment(detailTime))
|
|
|
+
|
|
|
+watch(todayValue, () => {
|
|
|
+ // 不进行点击及跳转
|
|
|
+ todayValue.value = moment(detailTime)
|
|
|
+})
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ initDailyReportData()
|
|
|
+})
|
|
|
+
|
|
|
+interface ItemState {
|
|
|
+ id?: string // ID
|
|
|
+ date: string // 日期
|
|
|
+ year: string // 年份
|
|
|
+ month: string // 月份
|
|
|
+ week: string // 第几周
|
|
|
+ dayOfWeek: string // 第几天
|
|
|
+ isworkday: number //是否工作日
|
|
|
+ state: string //leaveMap
|
|
|
+}
|
|
|
+// const weekNum = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
|
|
|
+// 考勤状态对应字典值 2.26
|
|
|
+const leaveMap = {
|
|
|
+ 1: '出勤',
|
|
|
+ 2: '迟到',
|
|
|
+ 3: '早退',
|
|
|
+ 4: '旷工',
|
|
|
+ 5: '出差',
|
|
|
+ 11: '事假',
|
|
|
+ 12: '病假',
|
|
|
+ 13: '婚假',
|
|
|
+ 14: '产假',
|
|
|
+ 15: '陪产假',
|
|
|
+ 19: '丧假',
|
|
|
+ 16: '工伤',
|
|
|
+ 17: '年假',
|
|
|
+ 18: '调休',
|
|
|
+ 20: '其他'
|
|
|
+}
|
|
|
+
|
|
|
+const dailyReportAttendances = ref<ItemState[]>([])
|
|
|
+const nowMonths: any = ref(moment(detailTime).format('YYYY-MM'))
|
|
|
+
|
|
|
+// const dictMap: any = getDictOptions(DICT_TYPE.ADM_ATTENDANCE_STATUS)
|
|
|
+const getWorkDayList = async (): Promise<any> => {
|
|
|
+ const nowTimeObj = moment(nowMonths.value)
|
|
|
+ const urlApi = `/adm/workday/list`
|
|
|
+ const params = {
|
|
|
+ dateDay: [
|
|
|
+ nowTimeObj.startOf('months').format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ nowTimeObj.endOf('months').format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ return await request.get({ url: urlApi, params })
|
|
|
+}
|
|
|
+
|
|
|
+const getAttendanceSheetListMine = async (): Promise<any> => {
|
|
|
+ const nowTimeObj = moment(nowMonths.value)
|
|
|
+ const urlApi = `/adm/attendance-sheet/list-me`
|
|
|
+ const params = {
|
|
|
+ attendanceTime: [
|
|
|
+ nowTimeObj.startOf('months').format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ nowTimeObj.endOf('months').format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ return await request.get({ url: urlApi, params })
|
|
|
+}
|
|
|
+
|
|
|
+// 日报统计
|
|
|
+const getLogList = async (): Promise<any> => {
|
|
|
+ const nowTimeObj = moment(nowMonths.value)
|
|
|
+ const urlApi = `/adm/report/list`
|
|
|
+ const params = {
|
|
|
+ reportType: 'daily',
|
|
|
+ reportYear: nowTimeObj.format('YYYY'),
|
|
|
+ reportMonth: Number(nowTimeObj.format('MM')),
|
|
|
+ userId: userInfo.id ?? ''
|
|
|
+ }
|
|
|
+ return await request.get({ url: urlApi, params })
|
|
|
+}
|
|
|
+
|
|
|
+const dayReportMap: any = ref({})
|
|
|
+const initDailyReportData = async (): Promise<any> => {
|
|
|
+ // 获取已填写的日志
|
|
|
+ const logList = await getLogList()
|
|
|
+ const logObj = {}
|
|
|
+ logList?.forEach((item) => {
|
|
|
+ logObj[moment(item.reportStartDate).format('YYYY-MM-DD')] = item
|
|
|
+ })
|
|
|
+ //考勤统计
|
|
|
+ const attendances: [] = await getAttendanceSheetListMine()
|
|
|
+ const attendanceState: Map<string, string> = new Map()
|
|
|
+ attendances.forEach((item) => {
|
|
|
+ if (leaveMap[item['attendanceStatus']]) {
|
|
|
+ attendanceState[moment(item['attendanceTime']).format('YYYY-MM-DD')] =
|
|
|
+ item['attendanceStatus']
|
|
|
+ }
|
|
|
+ })
|
|
|
+ //节假日统计
|
|
|
+ const workdays: any[] = await getWorkDayList()
|
|
|
+ const dailyReportList: any[] = []
|
|
|
+ workdays.forEach((item) => {
|
|
|
+ const date = moment(item['dateDay']).format('YYYY-MM-DD')
|
|
|
+ const _dailyReportMap = dailyReportList[date]
|
|
|
+ const _attendanceState = attendanceState[date]
|
|
|
+ const isLog = logObj[date] ?? false
|
|
|
+ if (_dailyReportMap) {
|
|
|
+ dailyReportList.push({
|
|
|
+ id: _dailyReportMap['id'],
|
|
|
+ date: moment(item['dateDay']).format('YYYY-MM-DD'),
|
|
|
+ year: item['year'],
|
|
|
+ month: item['month'],
|
|
|
+ week: item['week'],
|
|
|
+ dayOfWeek: item['dayOfWeek'],
|
|
|
+ isworkday: item['isworkday'],
|
|
|
+ state: _dailyReportMap['state'],
|
|
|
+ isLog,
|
|
|
+ holidayRemark: item['holidayRemark']
|
|
|
+ })
|
|
|
+ } else if (_attendanceState) {
|
|
|
+ dailyReportList.push({
|
|
|
+ id: '',
|
|
|
+ date: moment(item['dateDay']).format('YYYY-MM-DD'),
|
|
|
+ year: item['year'],
|
|
|
+ month: item['month'],
|
|
|
+ week: item['week'],
|
|
|
+ dayOfWeek: item['dayOfWeek'],
|
|
|
+ isworkday: item['isworkday'],
|
|
|
+ state: _attendanceState,
|
|
|
+ isLog,
|
|
|
+ holidayRemark: item['holidayRemark']
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ dailyReportList.push({
|
|
|
+ id: '',
|
|
|
+ date: moment(item['dateDay']).format('YYYY-MM-DD'),
|
|
|
+ year: item['year'],
|
|
|
+ month: item['month'],
|
|
|
+ week: item['week'],
|
|
|
+ dayOfWeek: item['dayOfWeek'],
|
|
|
+ isworkday: item['isworkday'],
|
|
|
+ state: item['isworkday'] === 0 ? '' : '-1',
|
|
|
+ isLog,
|
|
|
+ holidayRemark: item['holidayRemark']
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ dailyReportAttendances.value = dailyReportList
|
|
|
+ dailyReportList.forEach((item) => {
|
|
|
+ dayReportMap.value[item.date] = { ...item, show: item.date.split('-')[2] }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 日报样式设置
|
|
|
+ * 1、已填,绿色三角+对勾 square-filled
|
|
|
+ * state == 99
|
|
|
+ * 2、未填,红色三角+错号 square-unfilled
|
|
|
+ * state == -1
|
|
|
+ * 3、当天,蓝色三角+画笔 square-today
|
|
|
+ * 4、未到,灰色三角+省略 square-future
|
|
|
+ */
|
|
|
+const getDayClass = (data) => {
|
|
|
+ const { isworkday = 0, state = '', date = '' } = dayReportMap.value[data] ?? {}
|
|
|
+ const isFuture = moment(date).isAfter(moment())
|
|
|
+ // if (date == moment().format('YYYY-MM-DD')) {
|
|
|
+ // return `square-today`
|
|
|
+ // }
|
|
|
+ if (isworkday && state != -1 && !isFuture) {
|
|
|
+ return `square-filled`
|
|
|
+ } else if (isworkday && !isFuture) {
|
|
|
+ return `square-unfilled`
|
|
|
+ } else if (isworkday && isFuture) {
|
|
|
+ return `square-future`
|
|
|
+ }
|
|
|
+ return ''
|
|
|
+}
|
|
|
+
|
|
|
+// 鼠标移入移出事件
|
|
|
+const onMouseOver = (data) => {
|
|
|
+ const isLog = dayReportMap.value[data.day]?.isLog ? true : false
|
|
|
+ if (isLog) {
|
|
|
+ dayReportMap.value[data.day].show = '已填'
|
|
|
+ } else {
|
|
|
+ dayReportMap.value[data.day].show = dayReportMap.value[data.day]?.holidayRemark ?? '未填'
|
|
|
+ }
|
|
|
+}
|
|
|
+const onMouseOut = (data) => {
|
|
|
+ dayReportMap.value[data.day].show = data.day.split('-')[2]
|
|
|
+}
|
|
|
+</script>
|
|
|
+<style lang="scss" scoped>
|
|
|
+.OaCalendar {
|
|
|
+ width: 100%;
|
|
|
+ :deep(.el-calendar) {
|
|
|
+ .el-calendar__header {
|
|
|
+ padding: 0 !important;
|
|
|
+ border-bottom: none !important;
|
|
|
+ }
|
|
|
+ .el-calendar-table {
|
|
|
+ border: 1px solid #e1e3ea !important;
|
|
|
+ thead {
|
|
|
+ background: #f8faff !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .el-calendar-day {
|
|
|
+ padding: 0;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ .icon {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ text-align: center;
|
|
|
+ position: absolute;
|
|
|
+ top: 2px;
|
|
|
+ right: -1px;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: bold;
|
|
|
+ z-index: 999;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .day-cell {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ font-family: DINCond-Bold, DINCond-Bold;
|
|
|
+ ::before {
|
|
|
+ content: '';
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-left: 30px solid transparent;
|
|
|
+ border-bottom: 30px solid transparent;
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ right: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .square-filled {
|
|
|
+ ::before {
|
|
|
+ border-right: 30px solid #05ce9e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .square-unfilled {
|
|
|
+ ::before {
|
|
|
+ border-right: 30px solid #fc4308;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .square-today {
|
|
|
+ ::before {
|
|
|
+ border-right: 30px solid #1b80eb;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .square-future {
|
|
|
+ ::before {
|
|
|
+ border-right: 30px solid #c9cdd8;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .is-selected {
|
|
|
+ color: #1989fa;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .selectBox {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ .icon1 {
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ p {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #2d333c;
|
|
|
+ margin: 0 10px;
|
|
|
+ user-select: none;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .contentBox {
|
|
|
+ width: 100%;
|
|
|
+ height: 80px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ border: 1px solid #dee0e3;
|
|
|
+
|
|
|
+ .ulBox {
|
|
|
+ width: calc(100% - 120px);
|
|
|
+ height: 100%;
|
|
|
+ ul {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+ li {
|
|
|
+ height: 100%;
|
|
|
+ > div {
|
|
|
+ &:first-child {
|
|
|
+ width: 100%;
|
|
|
+ height: 50%;
|
|
|
+ border: 1px solid #dee0e3;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-right: 0;
|
|
|
+ border-top: 0;
|
|
|
+ background-color: #f7f8fa;
|
|
|
+ &.isToday {
|
|
|
+ background-color: #2e77e6;
|
|
|
+ > p {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .topBox {
|
|
|
+ p {
|
|
|
+ color: #121518;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .topBoxPa {
|
|
|
+ p {
|
|
|
+ color: #b9c3c9;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .topBox:hover {
|
|
|
+ p {
|
|
|
+ color: #1b80eb;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .bomBox {
|
|
|
+ width: 100%;
|
|
|
+ height: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ &.imgHover_-1,
|
|
|
+ &.imgHover_99 {
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ &.imgHover_-1:hover {
|
|
|
+ position: relative;
|
|
|
+ &::after {
|
|
|
+ position: absolute;
|
|
|
+ display: block;
|
|
|
+ content: '补';
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: #fff;
|
|
|
+ top: 0px;
|
|
|
+ left: 0px;
|
|
|
+ line-height: 40px;
|
|
|
+ text-align: center;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &.imgHover {
|
|
|
+ &:hover {
|
|
|
+ border: 1px solid #f00;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ img {
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+ > span {
|
|
|
+ cursor: pointer;
|
|
|
+ &.leave {
|
|
|
+ color: #f00;
|
|
|
+ cursor: default;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ li:first-child {
|
|
|
+ .topBox {
|
|
|
+ border-left: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ li:last-child {
|
|
|
+ .topBox {
|
|
|
+ border-right: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .infoBox {
|
|
|
+ width: 120px;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ background-color: #edf0f4;
|
|
|
+ border-left: 1px solid #dee0e3;
|
|
|
+ span {
|
|
|
+ display: block;
|
|
|
+ width: 100%;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ p {
|
|
|
+ width: 100%;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 18px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|