WXL
2025-12-28 1b736033f6d01b774d58b4c2d7cd2ce8607a44fa
src/views/OfficeRelated/checkingIn/components/AttendanceCalendar.vue
@@ -1,132 +1,23 @@
<template>
  <div class="attendance-calendar">
    <!-- 日历头部统计信息 -->
    <div class="calendar-stats">
      <div class="stat-item">
        <span class="stat-dot present"></span>
        <span>正常出勤: {{ stats.presentDays }}天</span>
      </div>
      <div class="stat-item">
        <span class="stat-dot absent"></span>
        <span>缺勤/异常: {{ stats.absentDays }}天</span>
      </div>
      <div class="stat-item">
        <span class="stat-dot trip"></span>
        <span>出差: {{ stats.tripDays }}天</span>
      </div>
      <div class="stat-item">
        <span class="stat-dot late"></span>
        <span>迟到/早退: {{ stats.lateDays }}天</span>
      </div>
    </div>
    <!-- Element UI 日历组件 -->
    <el-calendar v-model="calendarValue" :first-day-of-week="1">
    <el-calendar v-model="calendarValue">
      <template #date-cell="{ data }">
        <div
          class="calendar-date"
          :class="getDateStatusClass(data.day)"
          @click="handleDateClick(data)"
        >
        <div class="calendar-date">
          <div class="date-number">{{ data.day.split('-')[2] }}</div>
          <!-- 状态背景色块 -->
          <div class="status-background" :class="getBackgroundStatus(data.day)"></div>
          <div class="date-events">
            <!-- 出勤状态标记 -->
            <div v-if="getAttendanceStatus(data.day) !== 'absent'" class="status-mark">
              <el-tooltip
                :content="getAttendanceTooltip(data.day)"
                placement="top"
              >
                <span class="status-dot" :class="getAttendanceStatus(data.day)"></span>
              </el-tooltip>
            </div>
            <!-- 出差标记 -->
            <div v-if="hasBusinessTrip(data.day)" class="trip-mark">
              <el-tooltip content="出差" placement="top">
                <i class="el-icon-location-outline"></i>
              </el-tooltip>
            </div>
            <!-- 缺勤标记 -->
            <div v-if="getAttendanceStatus(data.day) === 'absent'" class="absent-mark">
              <el-tooltip content="缺勤" placement="top">
                <i class="el-icon-close"></i>
              </el-tooltip>
            </div>
          </div>
          <!-- 简略信息显示 -->
          <div class="brief-info">
            <div v-if="getAttendanceStatus(data.day) === 'present'" class="info-item present-info">
              √
            </div>
            <div v-else-if="getAttendanceStatus(data.day) === 'late'" class="info-item late-info">
              !
            </div>
            <div v-else-if="getAttendanceStatus(data.day) === 'absent'" class="info-item absent-info">
              ×
            </div>
            <div v-if="hasBusinessTrip(data.day)" class="info-item trip-info">
              ✈
            </div>
          </div>
          <!-- 日期详细信息(悬浮显示) -->
          <div class="date-details">
            <div
              v-for="event in getDateEvents(data.day)"
              :key="event.id"
              class="detail-item"
              class="event-item"
              :class="event.type"
            >
              <span class="detail-type">{{ event.type === 'attendance' ? '出勤' : '出差' }}</span>
              <span class="detail-info">{{ event.text }}</span>
            </div>
            <div v-if="getDateEvents(data.day).length === 0" class="detail-item">
              <span class="detail-type">无记录</span>
              <span class="event-dot"></span>
              <span class="event-text">{{ event.text }}</span>
            </div>
          </div>
        </div>
      </template>
    </el-calendar>
    <!-- 日期详情对话框 -->
    <el-dialog
      :title="`${selectedDate} 考勤详情`"
      v-model="detailDialogVisible"
      width="500px"
    >
      <div v-if="selectedDateInfo">
        <div class="detail-section">
          <h4>出勤信息</h4>
          <div v-if="selectedDateInfo.attendance">
            <p>上班时间: {{ selectedDateInfo.attendance.checkIn || '未打卡' }}</p>
            <p>下班时间: {{ selectedDateInfo.attendance.checkOut || '未打卡' }}</p>
            <p>状态:
              <el-tag :type="getStatusTagType(selectedDateInfo.attendance.status)">
                {{ getStatusText(selectedDateInfo.attendance.status) }}
              </el-tag>
            </p>
          </div>
          <div v-else>
            <p class="no-data">无出勤记录</p>
          </div>
        </div>
        <div class="detail-section" v-if="selectedDateInfo.businessTrip">
          <h4>出差信息</h4>
          <p>目的地: {{ selectedDateInfo.businessTrip.destination }}</p>
          <p>事由: {{ selectedDateInfo.businessTrip.reason }}</p>
          <p>里程: {{ selectedDateInfo.businessTrip.distance }}公里</p>
        </div>
      </div>
      <template #footer>
        <el-button @click="detailDialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
  </div>
</template>
@@ -134,211 +25,41 @@
export default {
  name: 'AttendanceCalendar',
  props: {
    attendanceData: {
      type: Array,
      default: () => []
    },
    businessTripData: {
      type: Array,
      default: () => []
    }
    attendanceData: Array,
    businessTripData: Array
  },
  data() {
    return {
      calendarValue: new Date(),
      detailDialogVisible: false,
      selectedDate: '',
      selectedDateInfo: null,
      stats: {
        presentDays: 0,
        absentDays: 0,
        tripDays: 0,
        lateDays: 0
      }
    }
  },
  mounted() {
    this.calculateStats()
  },
  watch: {
    attendanceData: {
      handler() {
        this.calculateStats()
      },
      deep: true
    },
    businessTripData: {
      handler() {
        this.calculateStats()
      },
      deep: true
      calendarValue: new Date()
    }
  },
  methods: {
    // 获取日期状态类名
    getDateStatusClass(date) {
      const classes = []
      if (this.isToday(date)) {
        classes.push('today')
      }
      if (this.isSelected(date)) {
        classes.push('selected')
      }
      // 添加状态类名
      const status = this.getBackgroundStatus(date)
      if (status) {
        classes.push(status)
      }
      return classes
    },
    // 获取背景状态(用于背景色)
    getBackgroundStatus(date) {
      const attendance = this.attendanceData.find(item => item.date === date)
      const hasTrip = this.hasBusinessTrip(date)
      if (hasTrip) {
        return 'has-trip'
      }
      if (attendance) {
        switch (attendance.status) {
          case 'present': return 'has-attendance'
          case 'late': return 'has-late'
          case 'absent': return 'has-absent'
          default: return ''
        }
      }
      return ''
    },
    // 判断是否为今天
    isToday(date) {
      const today = new Date()
      const compareDate = new Date(date)
      return today.toDateString() === compareDate.toDateString()
    },
    // 判断是否被选中
    isSelected(date) {
      return this.selectedDate === date
    },
    // 获取考勤状态
    getAttendanceStatus(date) {
      const attendance = this.attendanceData.find(item => item.date === date)
      if (!attendance) return 'absent'
      switch (attendance.status) {
        case 'present': return 'present'
        case 'late': return 'late'
        case 'absent': return 'absent'
        default: return 'absent'
      }
    },
    // 获取状态文本
    getStatusText(status) {
      const statusMap = {
        present: '正常出勤',
        late: '迟到/早退',
        absent: '缺勤/异常'
      }
      return statusMap[status] || '未知状态'
    },
    // 获取考勤状态提示
    getAttendanceTooltip(date) {
      const statusMap = {
        present: '正常出勤',
        late: '迟到/早退',
        absent: '缺勤/异常'
      }
      return statusMap[this.getAttendanceStatus(date)] || '无记录'
    },
    // 判断是否有出差
    hasBusinessTrip(date) {
      return this.businessTripData.some(item =>
        date >= item.startDate && date <= item.endDate
      )
    },
    // 获取日期事件
    getDateEvents(date) {
      const events = []
      const attendance = this.attendanceData.find(item => item.date === date)
      // 检查出勤记录
      const attendance = this.attendanceData.find(item => item.date === date)
      if (attendance) {
        events.push({
          id: `attendance-${date}`,
          type: 'attendance',
          text: `${attendance.checkIn || '未打卡'} - ${attendance.checkOut || '未打卡'}`
          text: `${attendance.checkIn}-${attendance.checkOut}`
        })
      }
      // 检查出差记录
      const businessTrip = this.businessTripData.find(item =>
        date >= item.startDate && date <= item.endDate
      )
      if (businessTrip) {
        events.push({
          id: `business-trip-${date}`,
          type: 'businessTrip',
          text: `前往${businessTrip.destination}`
          type: 'business-trip',
          text: `出差: ${businessTrip.endCity}`
        })
      }
      return events
    },
    // 处理日期点击事件
    handleDateClick(data) {
      this.selectedDate = data.day
      this.selectedDateInfo = {
        attendance: this.attendanceData.find(item => item.date === data.day),
        businessTrip: this.businessTripData.find(item =>
          data.day >= item.startDate && data.day <= item.endDate
        )
      }
      this.detailDialogVisible = true
    },
    // 获取状态标签类型
    getStatusTagType(status) {
      const typeMap = {
        present: 'success',
        late: 'warning',
        absent: 'danger'
      }
      return typeMap[status] || 'info'
    },
    // 计算统计信息
    calculateStats() {
      // 重置统计
      this.stats = { presentDays: 0, absentDays: 0, tripDays: 0, lateDays: 0 }
      this.attendanceData.forEach(item => {
        switch (item.status) {
          case 'present': this.stats.presentDays++; break
          case 'late': this.stats.lateDays++; break
          case 'absent': this.stats.absentDays++; break
        }
      })
      // 计算出差天数
      const tripDays = new Set()
      this.businessTripData.forEach(item => {
        const start = new Date(item.startDate)
        const end = new Date(item.endDate)
        for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
          tripDays.add(d.toISOString().split('T')[0])
        }
      })
      this.stats.tripDays = tripDays.size
    }
  }
}
@@ -347,242 +68,49 @@
<style scoped>
.attendance-calendar {
  padding: 20px;
  max-width: 100%;
}
.calendar-stats {
  display: flex;
  justify-content: space-around;
  margin-bottom: 20px;
  padding: 15px;
  background: #f5f7fa;
  border-radius: 8px;
}
.stat-item {
  display: flex;
  align-items: center;
  gap: 8px;
}
.stat-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  display: inline-block;
}
.stat-dot.present { background-color: #67c23a; }
.stat-dot.absent { background-color: #f56c6c; }
.stat-dot.trip { background-color: #409eff; }
.stat-dot.late { background-color: #e6a23c; }
.calendar-date {
  height: 80px;
  padding: 4px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
  position: relative;
  overflow: hidden;
}
.calendar-date:hover {
  background-color: #f0f9ff;
  border-color: #409eff;
  transform: translateY(-2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.calendar-date.today {
  border-color: #409eff;
  background-color: #f0f9ff;
}
.calendar-date.selected {
  background-color: #ecf5ff;
  border-color: #409eff;
}
/* 状态背景色 */
.status-background {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  opacity: 0.1;
  z-index: 0;
}
.calendar-date.has-attendance .status-background {
  background-color: #67c23a;
}
.calendar-date.has-late .status-background {
  background-color: #e6a23c;
}
.calendar-date.has-absent .status-background {
  background-color: #f56c6c;
}
.calendar-date.has-trip .status-background {
  background: linear-gradient(135deg, #409eff 0%, #67c23a 100%);
  height: 100%;
  display: flex;
  flex-direction: column;
}
.date-number {
  font-weight: bold;
  font-size: 14px;
  margin-bottom: 2px;
  position: relative;
  z-index: 1;
  margin-bottom: 4px;
}
.date-events {
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: relative;
  z-index: 1;
  flex: 1;
  overflow: hidden;
}
.status-mark, .trip-mark, .absent-mark {
.event-item {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-bottom: 2px;
  font-size: 12px;
}
.status-dot {
  width: 8px;
  height: 8px;
.event-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  display: inline-block;
}
.status-dot.present { background-color: #67c23a; }
.status-dot.late { background-color: #e6a23c; }
.status-dot.absent { background-color: #f56c6c; }
.trip-mark i {
  color: #409eff;
  font-size: 12px;
}
.absent-mark i {
  color: #f56c6c;
  font-size: 12px;
}
/* 简略信息显示 */
.brief-info {
  position: absolute;
  bottom: 4px;
  right: 4px;
  display: flex;
  gap: 2px;
  z-index: 1;
}
.info-item {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: bold;
}
.present-info {
  background-color: #67c23a;
  color: white;
}
.late-info {
  background-color: #e6a23c;
  color: white;
}
.absent-info {
  background-color: #f56c6c;
  color: white;
}
.trip-info {
  background-color: #409eff;
  color: white;
}
.date-details {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 8px;
  z-index: 1000;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  display: none;
}
.calendar-date:hover .date-details {
  display: block;
}
.detail-item {
  font-size: 12px;
  margin-bottom: 4px;
  display: flex;
  align-items: center;
}
.detail-type {
  font-weight: bold;
  margin-right: 4px;
  min-width: 40px;
}
.detail-info {
  color: #606266;
.event-item.attendance .event-dot {
  background-color: #67c23a;
}
.detail-section {
  margin-bottom: 20px;
.event-item.business-trip .event-dot {
  background-color: #409eff;
}
.detail-section h4 {
  margin-bottom: 10px;
  color: #303133;
  border-left: 4px solid #409eff;
  padding-left: 8px;
}
.no-data {
  color: #909399;
  font-style: italic;
}
:deep(.el-calendar__header) {
  padding: 10px;
  border-bottom: 1px solid #ebeef5;
}
:deep(.el-calendar-day) {
  padding: 0 !important;
  height: 80px;
}
:deep(.el-calendar-table:not(.is-range) td) {
  border: 1px solid #f0f0f0;
}
:deep(.el-calendar-table .el-calendar-day) {
  height: 80px !important;
  padding: 0 !important;
.event-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
</style>