WXL
2025-12-28 1b736033f6d01b774d58b4c2d7cd2ce8607a44fa
页面完善
已修改4个文件
已添加2个文件
3023 ■■■■ 文件已修改
src/views/OfficeRelated/checkingIn/checkingInInfo.vue 213 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/AttendanceCalendar.vue 542 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/PersonalAttendanceReport.vue 680 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/mockData.js 169 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/conference/index.vue 728 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/engage/index.vue 691 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/checkingInInfo.vue
@@ -4,12 +4,18 @@
    <el-card class="employee-info-card">
      <div class="employee-header">
        <div class="employee-basic">
          <el-avatar :size="60" :src="employeeInfo.avatar" class="employee-avatar">
          <el-avatar
            :size="60"
            :src="employeeInfo.avatar"
            class="employee-avatar"
          >
            {{ employeeInfo.name.charAt(0) }}
          </el-avatar>
          <div class="employee-details">
            <h3>{{ employeeInfo.name }}</h3>
            <p class="employee-department">{{ employeeInfo.department }} Â· {{ employeeInfo.position }}</p>
            <p class="employee-department">
              {{ employeeInfo.department }} Â· {{ employeeInfo.position }}
            </p>
            <p class="employee-contact">
              <span>工号: {{ employeeInfo.employeeId }}</span>
              <span>电话: {{ employeeInfo.phone }}</span>
@@ -36,13 +42,6 @@
    <!-- é€‰é¡¹å¡ -->
    <el-card>
      <el-tabs v-model="activeTab">
        <el-tab-pane label="日历视图" name="calendar">
          <attendance-calendar
            :attendance-data="attendanceData"
            :business-trip-data="businessTripData"
          />
        </el-tab-pane>
        <el-tab-pane label="出勤记录" name="attendanceList">
          <personal-attendance-table
            :data="attendanceData"
@@ -56,7 +55,12 @@
            :loading="loading"
          />
        </el-tab-pane>
        <el-tab-pane label="日历视图" name="calendar">
          <attendance-calendar
            :attendance-data="attendanceData"
            :business-trip-data="businessTripData"
          />
        </el-tab-pane>
        <el-tab-pane label="统计报表" name="report">
          <personal-attendance-report
            :stats="employeeStats"
@@ -69,15 +73,13 @@
</template>
<script>
  import { generateMockData } from './mockData'
import AttendanceCalendar from './components/AttendanceCalendar.vue'
import PersonalAttendanceTable from './components/PersonalAttendanceTable.vue'
import PersonBusiness from './components/PersonBusiness.vue'
import PersonalAttendanceReport from './components/PersonalAttendanceReport.vue'
import AttendanceCalendar from "./components/AttendanceCalendar.vue";
import PersonalAttendanceTable from "./components/PersonalAttendanceTable.vue";
import PersonBusiness from "./components/PersonBusiness.vue";
import PersonalAttendanceReport from "./components/PersonalAttendanceReport.vue";
export default {
  name: 'AttendanceDetail',
  name: "AttendanceDetail",
  components: {
    AttendanceCalendar,
    PersonalAttendanceTable,
@@ -86,15 +88,15 @@
  },
  data() {
    return {
       activeTab: 'calendar',
      activeTab: "calendar",
      loading: false,
      employeeInfo: {
        name: '张三',
        department: 'OPO项目部',
        position: '项目经理',
        employeeId: 'OPO001',
        phone: '138-1234-5678',
        avatar: ''
        name: "",
        department: "",
        position: "",
        employeeId: "",
        phone: "",
        avatar: ""
      },
      employeeStats: {
        attendanceRate: 0,
@@ -105,137 +107,120 @@
      },
      attendanceData: [],
      businessTripData: []
    }
    };
  },
  created() {
        this.loadMockData()
    this.getEmployeeInfo()
    this.loadAttendanceData()
    this.getEmployeeInfo();
    this.loadAttendanceData();
  },
  methods: {
    getEmployeeInfo() {
      const { employeeId, employeeName } = this.$route.query
      const { employeeId, employeeName } = this.$route.query;
      // æ¨¡æ‹Ÿå‘˜å·¥ä¿¡æ¯
      this.employeeInfo = {
        name: employeeName || '张三',
        department: 'OPO项目部',
        position: '项目经理',
        employeeId: employeeId || 'OPO001',
        phone: '138****1234',
        avatar: ''
      }
    },
 loadMockData() {
      this.loading = true
      // æ¨¡æ‹Ÿå¼‚步加载
      setTimeout(() => {
        const mockData = generateMockData()
        this.attendanceData = mockData.attendanceData
        this.businessTripData = mockData.businessTripData
        this.calculateStats()
        this.loading = false
      }, 500)
        name: employeeName || "张三",
        department: "OPO项目部",
        position: "项目经理",
        employeeId: employeeId || "OPO001",
        phone: "138****1234",
        avatar: ""
      };
    },
    calculateStats() {
      const totalDays = 31 // 12月有31天
      const attendanceDays = this.attendanceData.filter(item =>
        item.status === 'present' || item.status === 'late'
      ).length
      const lateTimes = this.attendanceData.filter(item =>
        item.status === 'late'
      ).length
      // è®¡ç®—出差总天数
      const businessTripDays = this.businessTripData.reduce((total, trip) => {
        const start = new Date(trip.startDate)
        const end = new Date(trip.endDate)
        const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
        return total + days
      }, 0)
      // è®¡ç®—总工作时长
      const totalWorkHours = this.attendanceData.reduce((total, item) => {
        return total + (item.workHours || 0)
      }, 0)
      this.employeeStats = {
        attendanceRate: Math.round((attendanceDays / totalDays) * 100),
        workHours: totalWorkHours.toFixed(1),
        businessTripDays: businessTripDays,
        lateTimes: lateTimes,
        leaveEarlyTimes: this.attendanceData.filter(item =>
          item.status === 'leaveEarly'
        ).length
      }
    },
    async loadAttendanceData() {
      this.loading = true
      this.loading = true;
      try {
        await new Promise(resolve => setTimeout(resolve, 500))
        await new Promise(resolve => setTimeout(resolve, 500));
        // ç”Ÿæˆä¸ªäººè€ƒå‹¤æ¨¡æ‹Ÿæ•°æ®
        this.attendanceData = this.generatePersonalAttendanceData()
        this.businessTripData = this.generatePersonalBusinessTripData()
        this.calculateStats()
        this.attendanceData = this.generatePersonalAttendanceData();
        this.businessTripData = this.generatePersonalBusinessTripData();
        this.calculateStats();
      } catch (error) {
        console.error('加载数据失败:', error)
        console.error("加载数据失败:", error);
      } finally {
        this.loading = false
        this.loading = false;
      }
    },
    generatePersonalAttendanceData() {
      const data = []
      const currentMonth = 12 // 12月
      const data = [];
      const currentMonth = 12; // 12月
      for (let day = 1; day <= 31; day++) {
        if (Math.random() > 0.2) { // 80%的出勤率
        if (Math.random() > 0.2) {
          // 80%的出勤率
          data.push({
            id: day,
            date: `2024-${currentMonth.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`,
            checkIn: `08:${String(Math.floor(Math.random() * 30)).padStart(2, '0')}`,
            checkOut: `18:${String(Math.floor(Math.random() * 30)).padStart(2, '0')}`,
            status: Math.random() > 0.1 ? '正常' : '迟到',
            date: `2024-${currentMonth
              .toString()
              .padStart(2, "0")}-${day.toString().padStart(2, "0")}`,
            checkIn: `08:${String(Math.floor(Math.random() * 30)).padStart(
              2,
              "0"
            )}`,
            checkOut: `18:${String(Math.floor(Math.random() * 30)).padStart(
              2,
              "0"
            )}`,
            status: Math.random() > 0.1 ? "正常" : "迟到",
            workHours: (8 + Math.random() * 2).toFixed(1)
          })
          });
        }
      }
      return data
      return data;
    },
    generatePersonalBusinessTripData() {
      return [
        {
          id: 1,
          tripNumber: 'BT202412001',
          startCity: '北京',
          endCity: '上海',
          startDate: '2024-12-05',
          endDate: '2024-12-08',
          tripNumber: "BT202412001",
          startCity: "北京",
          endCity: "上海",
          startDate: "2024-12-05",
          endDate: "2024-12-08",
          distance: 1200,
          purpose: '客户会议',
          status: '已完成'
          purpose: "客户会议",
          status: "已完成"
        },
        {
          id: 2,
          tripNumber: 'BT202412002',
          startCity: '北京',
          endCity: '广州',
          startDate: '2024-12-15',
          endDate: '2024-12-18',
          tripNumber: "BT202412002",
          startCity: "北京",
          endCity: "广州",
          startDate: "2024-12-15",
          endDate: "2024-12-18",
          distance: 1900,
          purpose: '项目调研',
          status: '已完成'
          purpose: "项目调研",
          status: "已完成"
        }
      ]
      ];
    },
    calculateStats() {
      const totalDays = 31;
      const attendanceDays = this.attendanceData.length;
      this.employeeStats = {
        attendanceRate: Math.round((attendanceDays / totalDays) * 100),
        workHours: this.attendanceData
          .reduce((sum, item) => sum + parseFloat(item.workHours), 0)
          .toFixed(1),
        businessTripDays: this.businessTripData.reduce((sum, item) => {
          const start = new Date(item.startDate);
          const end = new Date(item.endDate);
          return sum + Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
        }, 0),
        lateTimes: this.attendanceData.filter(item => item.status === "迟到")
          .length,
        leaveEarlyTimes: this.attendanceData.filter(
          item => item.status === "早退"
        ).length
      };
    }
  }
}
};
</script>
<style scoped>
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>
src/views/OfficeRelated/checkingIn/components/PersonalAttendanceReport.vue
@@ -12,6 +12,7 @@
          size="small"
          @click="exportReport"
          icon="el-icon-download"
          type="primary"
        >
          å¯¼å‡ºæŠ¥è¡¨
        </el-button>
@@ -20,7 +21,7 @@
    <!-- ç»Ÿè®¡æ¦‚览 -->
    <el-row :gutter="20" class="stats-overview">
      <el-col :span="6">
      <el-col :xs="12" :sm="6" class="stat-col">
        <el-card shadow="hover" class="stat-card">
          <div class="stat-content">
            <div class="stat-icon attendance-icon">
@@ -33,7 +34,7 @@
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
      <el-col :xs="12" :sm="6" class="stat-col">
        <el-card shadow="hover" class="stat-card">
          <div class="stat-content">
            <div class="stat-icon present-icon">
@@ -46,7 +47,7 @@
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
      <el-col :xs="12" :sm="6" class="stat-col">
        <el-card shadow="hover" class="stat-card">
          <div class="stat-content">
            <div class="stat-icon abnormal-icon">
@@ -59,7 +60,7 @@
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
      <el-col :xs="12" :sm="6" class="stat-col">
        <el-card shadow="hover" class="stat-card">
          <div class="stat-content">
            <div class="stat-icon rate-icon">
@@ -74,98 +75,150 @@
      </el-col>
    </el-row>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <!-- å›¾è¡¨åŒºåŸŸ - ä¼˜åŒ–布局 -->
    <el-row :gutter="20" class="charts-section">
      <el-col :span="12">
        <el-card header="出勤趋势" shadow="never">
      <el-col :xs="24" :lg="12" class="chart-col">
        <el-card class="chart-card" shadow="never">
          <template #header>
            <div class="chart-header">
              <span class="chart-title">出勤趋势分析</span>
              <div class="chart-legend">
                <span class="legend-item">
                  <span class="legend-color bar-color"></span>
                  å‡ºå‹¤å¤©æ•°
                </span>
                <span class="legend-item">
                  <span class="legend-color line-color"></span>
                  å‡ºå‹¤çއ
                </span>
              </div>
            </div>
          </template>
          <div id="attendanceTrendChart" class="chart-container"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card header="考勤分布" shadow="never">
      <el-col :xs="24" :lg="12" class="chart-col">
        <el-card class="chart-card" shadow="never">
          <template #header>
            <div class="chart-header">
              <span class="chart-title">考勤分布</span>
            </div>
          </template>
          <div id="attendanceDistributionChart" class="chart-container"></div>
        </el-card>
      </el-col>
    </el-row>
    <!-- è¯¦ç»†ç»Ÿè®¡è¡¨æ ¼ -->
    <el-card header="详细统计" class="detail-table-card" shadow="never">
      <el-table :data="detailedStats" border style="width: 100%">
        <el-table-column prop="month" label="月份"  />
        <el-table-column prop="workDays" label="应出勤天数"  />
        <el-table-column prop="actualDays" label="实际出勤"  />
        <el-table-column prop="lateTimes" label="迟到次数" >
          <template #default="scope">
            <el-tag v-if="scope.row.lateTimes > 0" type="warning" size="small">
              {{ scope.row.lateTimes }}
            </el-tag>
            <span v-else>{{ scope.row.lateTimes }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="leaveEarlyTimes" label="早退次数" >
          <template #default="scope">
            <el-tag v-if="scope.row.leaveEarlyTimes > 0" type="warning" size="small">
              {{ scope.row.leaveEarlyTimes }}
            </el-tag>
            <span v-else>{{ scope.row.leaveEarlyTimes }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="absenceDays" label="缺勤天数" >
          <template #default="scope">
            <el-tag v-if="scope.row.absenceDays > 0" type="danger" size="small">
              {{ scope.row.absenceDays }}
            </el-tag>
            <span v-else>{{ scope.row.absenceDays }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="businessTripDays" label="出差天数" >
          <template #default="scope">
            <el-tag v-if="scope.row.businessTripDays > 0" type="primary" size="small">
              {{ scope.row.businessTripDays }}
            </el-tag>
            <span v-else>{{ scope.row.businessTripDays }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="attendanceRate" label="出勤率" >
          <template #default="scope">
            <el-progress
              :percentage="scope.row.attendanceRate"
              :show-text="false"
              :color="getProgressColor(scope.row.attendanceRate)"
            />
            <span>{{ scope.row.attendanceRate }}%</span>
          </template>
        </el-table-column>
      </el-table>
    <el-card class="detail-card" shadow="never">
      <template #header>
        <div class="card-header">
          <span class="card-title">月度详细统计</span>
          <el-button size="mini" type="text" @click="toggleTableExpand">
            {{ tableExpanded ? '收起' : '展开' }}
            <i :class="tableExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
          </el-button>
        </div>
      </template>
      <el-collapse-transition>
        <div v-show="tableExpanded">
          <el-table
            :data="detailedStats"
            border
            style="width: 100%"
            class="stats-table"
            :row-class-name="getRowClassName"
          >
            <!-- è¡¨æ ¼åˆ—定义保持不变 -->
            <el-table-column prop="month" label="月份" align="center" />
            <el-table-column prop="workDays" label="应出勤天数" align="center" />
            <el-table-column prop="actualDays" label="实际出勤" align="center" />
            <el-table-column prop="lateTimes" label="迟到次数" align="center">
              <template #default="scope">
                <el-tag v-if="scope.row.lateTimes > 0" type="warning" size="small">
                  {{ scope.row.lateTimes }}
                </el-tag>
                <span v-else>{{ scope.row.lateTimes }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="leaveEarlyTimes" label="早退次数" align="center">
              <template #default="scope">
                <el-tag v-if="scope.row.leaveEarlyTimes > 0" type="warning" size="small">
                  {{ scope.row.leaveEarlyTimes }}
                </el-tag>
                <span v-else>{{ scope.row.leaveEarlyTimes }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="absenceDays" label="缺勤天数" align="center">
              <template #default="scope">
                <el-tag v-if="scope.row.absenceDays > 0" type="danger" size="small">
                  {{ scope.row.absenceDays }}
                </el-tag>
                <span v-else>{{ scope.row.absenceDays }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="businessTripDays" label="出差天数" align="center">
              <template #default="scope">
                <el-tag v-if="scope.row.businessTripDays > 0" type="primary" size="small">
                  {{ scope.row.businessTripDays }}
                </el-tag>
                <span v-else>{{ scope.row.businessTripDays }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="attendanceRate" label="出勤率" align="center" min-width="100">
              <template #default="scope">
                <div class="progress-cell">
                  <el-progress
                    :percentage="scope.row.attendanceRate"
                    :show-text="false"
                    :color="getProgressColor(scope.row.attendanceRate)"
                    class="rate-progress"
                  />
                  <span class="rate-text" :class="getRateTextClass(scope.row.attendanceRate)">
                    {{ scope.row.attendanceRate }}%
                  </span>
                </div>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-collapse-transition>
    </el-card>
    <!-- å¼‚常记录 -->
    <el-card header="异常记录" class="abnormal-records-card" shadow="never">
      <el-table :data="abnormalRecords" border style="width: 100%">
        <el-table-column prop="date" label="日期"  />
        <el-table-column prop="type" label="异常类型" >
    <el-card class="abnormal-card" shadow="never">
      <template #header>
        <div class="card-header">
          <span class="card-title">异常记录明细</span>
        </div>
      </template>
      <el-table :data="abnormalRecords" border class="abnormal-table">
        <!-- å¼‚常记录表格列定义保持不变 -->
        <el-table-column prop="date" label="日期" align="center" />
        <el-table-column prop="type" label="异常类型" align="center">
          <template #default="scope">
            <el-tag :type="getAbnormalType(scope.row.type)" size="small">
            <el-tag :type="getAbnormalType(scope.row.type)" size="small" effect="light">
              {{ scope.row.type }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="description" label="描述" min-width="200" />
        <el-table-column prop="duration" label="时长"  />
        <el-table-column prop="status" label="状态" >
        <el-table-column prop="description" label="描述" align="center" min-width="200" />
        <el-table-column prop="duration" label="时长" align="center" />
        <el-table-column prop="status" label="状态" align="center">
          <template #default="scope">
            <el-tag
              :type="scope.row.status === '已处理' ? 'success' : 'warning'"
              size="small"
              effect="light"
            >
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" >
        <el-table-column label="操作" align="center" width="80">
          <template #default="scope">
            <el-button type="text" size="mini" @click="viewAbnormalDetail(scope.row)">
              æŸ¥çœ‹
              è¯¦æƒ…
            </el-button>
          </template>
        </el-table-column>
@@ -201,7 +254,8 @@
      detailedStats: [],
      abnormalRecords: [],
      trendChart: null,
      distributionChart: null
      distributionChart: null,
      tableExpanded: true
    }
  },
  mounted() {
@@ -211,7 +265,7 @@
    })
  },
  beforeDestroy() {
    // é”€æ¯å›¾è¡¨å®žä¾‹
    // é”€æ¯å›¾è¡¨å®žä¾‹[3](@ref)
    if (this.trendChart) {
      this.trendChart.dispose()
    }
@@ -221,7 +275,7 @@
  },
  methods: {
    initData() {
      // åˆå§‹åŒ–概览数据
      // åˆå§‹åŒ–数据逻辑保持不变
      this.overview = {
        totalDays: this.stats.totalDays || 22,
        presentDays: this.stats.presentDays || 20,
@@ -229,7 +283,6 @@
        attendanceRate: this.stats.attendanceRate || 90.9
      }
      // åˆå§‹åŒ–详细统计
      this.detailedStats = [
        { month: '2024-12', workDays: 22, actualDays: 20, lateTimes: 2,
          leaveEarlyTimes: 1, absenceDays: 0, businessTripDays: 3, attendanceRate: 90.9 },
@@ -239,7 +292,6 @@
          leaveEarlyTimes: 0, absenceDays: 0, businessTripDays: 1, attendanceRate: 95.7 }
      ]
      // åˆå§‹åŒ–异常记录
      this.abnormalRecords = [
        { date: '2024-12-15', type: '迟到', description: '早上迟到30分钟', duration: '30分钟', status: '已处理' },
        { date: '2024-12-08', type: '早退', description: '下午提前1小时离开', duration: '1小时', status: '已处理' },
@@ -261,43 +313,89 @@
      const option = {
        tooltip: {
          trigger: 'axis',
          backgroundColor: 'rgba(255, 255, 255, 0.95)',
          borderColor: '#ebeef5',
          borderWidth: 1,
          textStyle: {
            color: '#606266'
          },
          formatter: function(params) {
            let result = params[0].axisValue + '<br/>'
            let result = `<div style="font-weight: 600; margin-bottom: 8px;">${params[0].axisValue}</div>`
            params.forEach(param => {
              result += `${param.seriesName}: ${param.value}<br/>`
              const icon = param.seriesType === 'bar' ? '●' : '◆'
              result += `<div>${icon} ${param.seriesName}: <span style="font-weight: 600; color: ${param.color}">${param.value}${param.seriesName === '出勤率' ? '%' : ''}</span></div>`
            })
            return result
          }
        },
        legend: {
          data: ['出勤天数', '异常天数', '出勤率'],
          bottom: 10
          bottom: 10,
          textStyle: {
            color: '#606266'
          },
          itemWidth: 12,
          itemHeight: 12
        },
        grid: {
          left: '3%',
          right: '4%',
          right: '3%',
          bottom: '15%',
          top: '10%',
          top: '15%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: ['10月', '11月', '12月']
          data: ['10月', '11月', '12月'],
          axisLine: {
            lineStyle: {
              color: '#dcdfe6'
            }
          },
          axisLabel: {
            color: '#606266'
          }
        },
        yAxis: [
          {
            type: 'value',
            name: '天数',
            min: 0,
            max: 30
            max: 30,
            axisLine: {
              show: true,
              lineStyle: {
                color: '#dcdfe6'
              }
            },
            axisLabel: {
              color: '#606266',
              formatter: '{value}'
            },
            splitLine: {
              lineStyle: {
                color: '#f0f2f5',
                type: 'dashed'
              }
            }
          },
          {
            type: 'value',
            name: '出勤率(%)',
            min: 0,
            max: 100,
            axisLine: {
              show: true,
              lineStyle: {
                color: '#dcdfe6'
              }
            },
            axisLabel: {
              color: '#606266',
              formatter: '{value}%'
            },
            splitLine: {
              show: false
            }
          }
        ],
@@ -305,19 +403,39 @@
          {
            name: '出勤天数',
            type: 'bar',
            barWidth: '30%',
            barWidth: '25%',
            data: [22, 19, 20],
            itemStyle: {
              color: '#409EFF'
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: '#409EFF' },
                { offset: 1, color: '#66b1ff' }
              ]),
              borderRadius: [2, 2, 0, 0]
            },
            emphasis: {
              itemStyle: {
                shadowBlur: 10,
                shadowColor: 'rgba(64, 158, 255, 0.5)'
              }
            }
          },
          {
            name: '异常天数',
            type: 'bar',
            barWidth: '30%',
            barWidth: '25%',
            data: [1, 2, 2],
            itemStyle: {
              color: '#F56C6C'
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: '#F56C6C' },
                { offset: 1, color: '#f78989' }
              ]),
              borderRadius: [2, 2, 0, 0]
            },
            emphasis: {
              itemStyle: {
                shadowBlur: 10,
                shadowColor: 'rgba(245, 108, 108, 0.5)'
              }
            }
          },
          {
@@ -325,14 +443,28 @@
            type: 'line',
            yAxisIndex: 1,
            data: [95.7, 90.5, 90.9],
            symbol: 'circle',
            symbolSize: 8,
            itemStyle: {
              color: '#67C23A'
              color: '#67C23A',
              borderColor: '#fff',
              borderWidth: 2
            },
            lineStyle: {
              width: 3
            }
              width: 3,
              shadowColor: 'rgba(103, 194, 58, 0.3)',
              shadowBlur: 8,
              shadowOffsetY: 2
            },
            emphasis: {
              scale: true
            },
            animationEasing: 'cubicInOut',
            animationDuration: 2000
          }
        ]
        ],
        animation: true,
        animationDuration: 1500
      }
      this.trendChart.setOption(option)
    },
@@ -345,13 +477,27 @@
      const option = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b}: {c} ({d}%)'
          backgroundColor: 'rgba(255, 255, 255, 0.95)',
          borderColor: '#ebeef5',
          borderWidth: 1,
          textStyle: {
            color: '#606266'
          },
          formatter: '{a} <br/>{b}: {c}天 ({d}%)'
        },
        legend: {
          orient: 'vertical',
          right: 10,
          top: 'center',
          data: ['正常出勤', '迟到', '早退', '缺勤', '出差']
          textStyle: {
            color: '#606266',
            fontSize: 12
          },
          itemWidth: 12,
          itemHeight: 12,
          formatter: function(name) {
            return `${name}`
          }
        },
        series: [
          {
@@ -359,24 +505,38 @@
            type: 'pie',
            radius: ['40%', '70%'],
            center: ['40%', '50%'],
            avoidLabelOverlap: false,
            avoidLabelOverlap: true,
            itemStyle: {
              borderColor: '#fff',
              borderWidth: 2
              borderWidth: 2,
              borderRadius: 3,
              shadowBlur: 5,
              shadowColor: 'rgba(0, 0, 0, 0.1)'
            },
            label: {
              show: false,
              position: 'center'
              show: true,
              formatter: '{b}\n{d}%',
              textStyle: {
                fontSize: 12,
                fontWeight: 'normal'
              }
            },
            emphasis: {
              label: {
                show: true,
                fontSize: 18,
                fontSize: 14,
                fontWeight: 'bold'
              },
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: 'rgba(0, 0, 0, 0.2)'
              }
            },
            labelLine: {
              show: false
              length: 15,
              length2: 10,
              smooth: true
            },
            data: [
              { value: 20, name: '正常出勤', itemStyle: { color: '#67C23A' } },
@@ -384,23 +544,30 @@
              { value: 1, name: '早退', itemStyle: { color: '#F56C6C' } },
              { value: 0, name: '缺勤', itemStyle: { color: '#909399' } },
              { value: 3, name: '出差', itemStyle: { color: '#409EFF' } }
            ]
            ],
            animationType: 'scale',
            animationEasing: 'elasticOut',
            animationDelay: function(idx) {
              return Math.random() * 200
            }
          }
        ]
        ],
        animation: true,
        animationDuration: 1000
      }
      this.distributionChart.setOption(option)
    },
    setupChartResize() {
      // ç›‘听窗口变化,重新渲染图表
      const handleResize = () => {
      // ä½¿ç”¨èŠ‚æµå‡½æ•°ä¼˜åŒ–æ€§èƒ½[3](@ref)
      const handleResize = this.throttle(() => {
        if (this.trendChart) {
          this.trendChart.resize()
        }
        if (this.distributionChart) {
          this.distributionChart.resize()
        }
      }
      }, 300)
      window.addEventListener('resize', handleResize)
      this.$once('hook:beforeDestroy', () => {
@@ -408,13 +575,42 @@
      })
    },
    throttle(func, wait) {
      let timeout = null
      return function() {
        const context = this
        const args = arguments
        if (!timeout) {
          timeout = setTimeout(() => {
            timeout = null
            func.apply(context, args)
          }, wait)
        }
      }
    },
    toggleTableExpand() {
      this.tableExpanded = !this.tableExpanded
    },
    getRowClassName({ rowIndex }) {
      return rowIndex % 2 === 1 ? 'even-row' : 'odd-row'
    },
    getRateTextClass(rate) {
      if (rate >= 95) return 'rate-excellent'
      if (rate >= 90) return 'rate-good'
      if (rate >= 80) return 'rate-average'
      return 'rate-poor'
    },
    // å…¶ä»–方法保持不变
    handlePeriodChange(period) {
      this.reportPeriod = period
      this.updateChartData()
    },
    updateChartData() {
      // æ ¹æ®é€‰æ‹©çš„周期更新图表数据
      let data
      switch (this.reportPeriod) {
        case 'month':
@@ -429,12 +625,10 @@
        default:
          data = this.getMonthlyData()
      }
      this.updateCharts(data)
    },
    getMonthlyData() {
      // æ¨¡æ‹Ÿæœˆåº¦æ•°æ®
      return {
        xAxis: ['10月', '11月', '12月'],
        attendance: [22, 19, 20],
@@ -444,7 +638,6 @@
    },
    getQuarterlyData() {
      // æ¨¡æ‹Ÿå­£åº¦æ•°æ®
      return {
        xAxis: ['Q1', 'Q2', 'Q3', 'Q4'],
        attendance: [65, 62, 58, 61],
@@ -454,7 +647,6 @@
    },
    getYearlyData() {
      // æ¨¡æ‹Ÿå¹´åº¦æ•°æ®
      return {
        xAxis: ['2022', '2023', '2024'],
        attendance: [240, 248, 252],
@@ -470,7 +662,7 @@
        option.series[0].data = data.attendance
        option.series[1].data = data.abnormal
        option.series[2].data = data.rate
        this.trendChart.setOption(option)
        this.trendChart.setOption(option, { notMerge: false })
      }
    },
@@ -493,12 +685,10 @@
    viewAbnormalDetail(record) {
      this.$message.info(`查看异常记录: ${record.date} - ${record.type}`)
      // è¿™é‡Œå¯ä»¥æ‰“开详情对话框
    },
    exportReport() {
      this.$message.success('报表导出功能开发中')
      // è¿™é‡Œå¯ä»¥å®žçŽ°å¯¼å‡ºPDF或Excel功能
    }
  }
}
@@ -506,9 +696,9 @@
<style scoped>
.personal-attendance-report {
  padding: 20px;
  background: #fff;
  border-radius: 8px;
  padding: 24px;
  background: #f8f9fa;
  min-height: 100vh;
}
.report-header {
@@ -516,15 +706,17 @@
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid #ebeef5;
  padding: 0;
}
.report-header h4 {
  margin: 0;
  color: #303133;
  font-size: 20px;
  font-size: 24px;
  font-weight: 600;
  background: linear-gradient(135deg, #409EFF, #67C23A);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
.header-actions {
@@ -534,51 +726,78 @@
}
.stats-overview {
  margin-bottom: 24px;
  margin-bottom: 32px;
}
.stat-col {
  margin-bottom: 20px;
}
.stat-card {
  border-radius: 8px;
  transition: all 0.3s ease;
  border-radius: 12px;
  border: none;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  background: #fff;
  position: relative;
  overflow: hidden;
}
.stat-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--gradient-start), var(--gradient-end));
}
.stat-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transform: translateY(-4px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
}
.stat-content {
  display: flex;
  align-items: center;
  padding: 16px;
  padding: 20px;
}
.stat-icon {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  width: 64px;
  height: 64px;
  border-radius: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 16px;
  font-size: 24px;
  font-size: 28px;
  color: white;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.attendance-icon {
  background: linear-gradient(135deg, #409EFF, #79BBFF);
  --gradient-start: #409EFF;
  --gradient-end: #66b1ff;
  background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
}
.present-icon {
  background: linear-gradient(135deg, #67C23A, #95D475);
  --gradient-start: #67C23A;
  --gradient-end: #85ce61;
  background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
}
.abnormal-icon {
  background: linear-gradient(135deg, #E6A23C, #EEBD6D);
  --gradient-start: #E6A23C;
  --gradient-end: #ebb563;
  background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
}
.rate-icon {
  background: linear-gradient(135deg, #F56C6C, #F89898);
  --gradient-start: #F56C6C;
  --gradient-end: #f78989;
  background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
}
.stat-info {
@@ -586,47 +805,162 @@
}
.stat-value {
  font-size: 28px;
  font-weight: bold;
  font-size: 32px;
  font-weight: 700;
  color: #303133;
  margin-bottom: 4px;
  line-height: 1;
}
.stat-label {
  color: #909399;
  font-size: 14px;
  font-weight: 500;
}
.charts-section {
  margin-bottom: 32px;
}
.chart-col {
  margin-bottom: 24px;
}
.chart-container {
  width: 100%;
  height: 300px;
.chart-card {
  border-radius: 12px;
  border: none;
  background: #fff;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.detail-table-card,
.abnormal-records-card {
  margin-bottom: 20px;
.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0;
}
.chart-title {
  font-size: 16px;
  font-weight: 600;
  color: #303133;
}
.chart-legend {
  display: flex;
  gap: 16px;
  align-items: center;
}
.legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: #606266;
}
.legend-color {
  width: 12px;
  height: 12px;
  border-radius: 2px;
}
.bar-color {
  background: linear-gradient(135deg, #409EFF, #66b1ff);
}
.line-color {
  background: linear-gradient(135deg, #67C23A, #85ce61);
}
.chart-container {
  width: 40vw;
  height: 320px;
  position: relative;
}
.detail-card,
.abnormal-card {
  border-radius: 12px;
  border: none;
  background: #fff;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  margin-bottom: 24px;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0;
}
.card-title {
  font-size: 16px;
  font-weight: 600;
  color: #303133;
}
.stats-table {
  border-radius: 8px;
  overflow: hidden;
}
.stats-table :deep(.el-table__row) {
  transition: background-color 0.3s;
}
.stats-table :deep(.el-table__row:hover) {
  background-color: #f5f7fa;
}
.stats-table :deep(.even-row) {
  background-color: #fafbfc;
}
.progress-cell {
  display: flex;
  align-items: center;
  gap: 8px;
}
.rate-progress {
  flex: 1;
}
.rate-text {
  font-size: 12px;
  font-weight: 600;
  min-width: 40px;
}
.rate-excellent { color: #67C23A; }
.rate-good { color: #E6A23C; }
.rate-average { color: #F56C6C; }
.rate-poor { color: #909399; }
.abnormal-table {
  border-radius: 8px;
  overflow: hidden;
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
  .stats-overview .el-col {
    margin-bottom: 16px;
  .chart-container {
    height: 280px;
  }
}
@media (max-width: 768px) {
  .personal-attendance-report {
    padding: 12px;
    padding: 16px;
  }
  .report-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
    gap: 16px;
  }
  .header-actions {
@@ -635,14 +969,42 @@
  }
  .stat-content {
    padding: 12px;
    padding: 16px;
    flex-direction: column;
    text-align: center;
  }
  .stat-icon {
    width: 50px;
    height: 50px;
    font-size: 20px;
    margin-right: 12px;
    margin-right: 0;
    margin-bottom: 12px;
    width: 56px;
    height: 56px;
    font-size: 24px;
  }
  .stat-value {
    font-size: 28px;
  }
  .chart-container {
    height: 240px;
  }
  .chart-header {
    flex-direction: column;
    gap: 12px;
    align-items: flex-start;
  }
  .chart-legend {
    align-self: stretch;
    justify-content: space-around;
  }
}
@media (max-width: 480px) {
  .stat-col {
    margin-bottom: 16px;
  }
  .stat-value {
@@ -650,7 +1012,7 @@
  }
  .chart-container {
    height: 250px;
    height: 200px;
  }
}
@@ -662,21 +1024,23 @@
  opacity: 0;
}
/* è¡¨æ ¼æ ·å¼ä¼˜åŒ– */
.el-table {
  border-radius: 4px;
  overflow: hidden;
/* æ»šåŠ¨æ¡æ ·å¼ä¼˜åŒ– */
:deep(::-webkit-scrollbar) {
  width: 6px;
  height: 6px;
}
.el-table::before {
  display: none;
:deep(::-webkit-scrollbar-track) {
  background: #f1f1f1;
  border-radius: 3px;
}
/* å¡ç‰‡æ ‡é¢˜æ ·å¼ */
.el-card__header {
  background: #f8f9fa;
  border-bottom: 1px solid #ebeef5;
  font-weight: 600;
  color: #303133;
:deep(::-webkit-scrollbar-thumb) {
  background: #c1c1c1;
  border-radius: 3px;
}
:deep(::-webkit-scrollbar-thumb:hover) {
  background: #a8a8a8;
}
</style>
src/views/OfficeRelated/checkingIn/mockData.js
@@ -1,131 +1,52 @@
// åœ¨çˆ¶ç»„ä»¶ data() ä¸­æˆ–单独创建 mockData.js æ–‡ä»¶
export const generateMockData = () => {
  return {
    // å‘˜å·¥è€ƒå‹¤æ•°æ®
    // å‘˜å·¥è€ƒå‹¤æ•°æ® - éœ€è¦è¡¥å……完整月份的数据
    attendanceData: [
      {
        id: 1,
        date: '2024-12-01',
        checkIn: '08:30',
        checkOut: '18:00',
        status: 'present',
        workHours: 9.5
      },
      {
        id: 2,
        date: '2024-12-02',
        checkIn: '09:15',
        checkOut: '18:00',
        status: 'late',
        workHours: 8.75
      },
      {
        id: 3,
        date: '2024-12-03',
        checkIn: '08:45',
        checkOut: '17:30',
        status: 'present',
        workHours: 8.75
      },
      {
        id: 4,
        date: '2024-12-04',
        checkIn: '08:25',
        checkOut: '18:10',
        status: 'present',
        workHours: 9.75
      },
      {
        id: 5,
        date: '2024-12-05',
        checkIn: null,
        checkOut: null,
        status: 'absent',
        workHours: 0
      },
      {
        id: 6,
        date: '2024-12-08',
        checkIn: '08:40',
        checkOut: '17:45',
        status: 'present',
        workHours: 9.0
      },
      {
        id: 7,
        date: '2024-12-09',
        checkIn: '08:35',
        checkOut: '18:05',
        status: 'present',
        workHours: 9.5
      },
      {
        id: 8,
        date: '2024-12-10',
        checkIn: '09:05',
        checkOut: '17:50',
        status: 'late',
        workHours: 8.75
      },
      {
        id: 9,
        date: '2024-12-11',
        checkIn: '08:50',
        checkOut: '18:15',
        status: 'present',
        workHours: 9.5
      },
      {
        id: 10,
        date: '2024-12-12',
        checkIn: '08:30',
        checkOut: '17:40',
        status: 'present',
        workHours: 9.0
      },
      {
        id: 11,
        date: '2024-12-15',
        checkIn: '08:45',
        checkOut: '18:00',
        status: 'present',
        workHours: 9.25
      },
      {
        id: 12,
        date: '2024-12-16',
        checkIn: '08:55',
        checkOut: '17:55',
        status: 'present',
        workHours: 9.0
      },
      {
        id: 13,
        date: '2024-12-17',
        checkIn: '08:40',
        checkOut: '18:10',
        status: 'present',
        workHours: 9.5
      },
      {
        id: 14,
        date: '2024-12-18',
        checkIn: '09:20',
        checkOut: '17:30',
        status: 'late',
        workHours: 8.0
      },
      {
        id: 15,
        date: '2024-12-19',
        checkIn: '08:35',
        checkOut: '18:05',
        status: 'present',
        workHours: 9.5
      }
      // 12月1-4日
      { id: 1, date: '2024-12-01', checkIn: '08:30', checkOut: '18:00', status: 'present', workHours: 9.5 },
      { id: 2, date: '2024-12-02', checkIn: '09:15', checkOut: '18:00', status: 'late', workHours: 8.75 },
      { id: 3, date: '2024-12-03', checkIn: '08:45', checkOut: '17:30', status: 'present', workHours: 8.75 },
      { id: 4, date: '2024-12-04', checkIn: '08:25', checkOut: '18:10', status: 'present', workHours: 9.75 },
      // 12月5-8日(5号缺勤,5-8号出差)
      { id: 5, date: '2024-12-05', checkIn: null, checkOut: null, status: 'absent', workHours: 0 },
      { id: 6, date: '2024-12-06', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
      { id: 7, date: '2024-12-07', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
      { id: 8, date: '2024-12-08', checkIn: '08:40', checkOut: '17:45', status: 'present', workHours: 9.0 },
      // 12月9-14日
      { id: 9, date: '2024-12-09', checkIn: '08:35', checkOut: '18:05', status: 'present', workHours: 9.5 },
      { id: 10, date: '2024-12-10', checkIn: '09:05', checkOut: '17:50', status: 'late', workHours: 8.75 },
      { id: 11, date: '2024-12-11', checkIn: '08:50', checkOut: '18:15', status: 'present', workHours: 9.5 },
      { id: 12, date: '2024-12-12', checkIn: '08:30', checkOut: '17:40', status: 'present', workHours: 9.0 },
      { id: 13, date: '2024-12-13', checkIn: '08:55', checkOut: '18:00', status: 'present', workHours: 9.0 },
      { id: 14, date: '2024-12-14', checkIn: '08:45', checkOut: '17:50', status: 'present', workHours: 9.0 },
      // 12月15-18日(出差)
      { id: 15, date: '2024-12-15', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
      { id: 16, date: '2024-12-16', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
      { id: 17, date: '2024-12-17', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
      { id: 18, date: '2024-12-18', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
      // 12月19-24日
      { id: 19, date: '2024-12-19', checkIn: '08:35', checkOut: '18:05', status: 'present', workHours: 9.5 },
      { id: 20, date: '2024-12-20', checkIn: '08:40', checkOut: '17:55', status: 'present', workHours: 9.25 },
      { id: 21, date: '2024-12-21', checkIn: null, checkOut: null, status: 'absent', workHours: 0 }, // å‘¨å…­
      { id: 22, date: '2024-12-22', checkIn: null, checkOut: null, status: 'trip', workHours: 0 }, // å‡ºå·®
      { id: 23, date: '2024-12-23', checkIn: null, checkOut: null, status: 'trip', workHours: 0 }, // å‡ºå·®
      { id: 24, date: '2024-12-24', checkIn: null, checkOut: null, status: 'trip', workHours: 0 }, // å‡ºå·®
      // 12月25-31日
      { id: 25, date: '2024-12-25', checkIn: '08:50', checkOut: '17:45', status: 'present', workHours: 8.75 },
      { id: 26, date: '2024-12-26', checkIn: '09:10', checkOut: '17:40', status: 'late', workHours: 8.5 },
      { id: 27, date: '2024-12-27', checkIn: '08:35', checkOut: '18:00', status: 'present', workHours: 9.25 },
      { id: 28, date: '2024-12-28', checkIn: '08:45', checkOut: '17:50', status: 'present', workHours: 9.0 },
      { id: 29, date: '2024-12-29', checkIn: null, checkOut: null, status: 'absent', workHours: 0 }, // å‘¨æ—¥
      { id: 30, date: '2024-12-30', checkIn: '08:30', checkOut: '17:30', status: 'present', workHours: 9.0 },
      { id: 31, date: '2024-12-31', checkIn: '08:40', checkOut: '16:00', status: 'present', workHours: 7.5 }
    ],
    // å‡ºå·®æ•°æ®
    // å‡ºå·®æ•°æ® - éœ€è¦è°ƒæ•´æ—¥æœŸèŒƒå›´åŒ¹é…
    businessTripData: [
      {
        id: 1,
@@ -133,7 +54,7 @@
        startCity: '北京',
        endCity: '上海',
        startDate: '2024-12-05',
        endDate: '2024-12-08',
        endDate: '2024-12-07', // è°ƒæ•´ä¸º7号结束
        distance: 1200,
        purpose: '客户会议',
        status: 'completed'
src/views/OfficeRelated/conference/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,728 @@
<template>
  <div class="meeting-management">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <div class="page-header">
      <h2>会议管理</h2>
      <div class="header-actions">
        <el-button type="primary" icon="el-icon-plus" @click="handleAdd">
          æ–°å»ºä¼šè®®
        </el-button>
        <el-button icon="el-icon-download" @click="exportData">
          å¯¼å‡ºæ•°æ®
        </el-button>
      </div>
    </div>
    <!-- æœç´¢ç­›é€‰åŒºåŸŸ -->
    <el-card class="filter-card">
      <el-form :model="queryParams" inline>
        <el-form-item label="会议类型">
          <el-select v-model="queryParams.meetingType" clearable placeholder="请选择">
            <el-option label="科研会议" value="research" />
            <el-option label="日常会议" value="daily" />
            <el-option label="项目会议" value="project" />
            <el-option label="部门会议" value="department" />
            <el-option label="评审会议" value="review" />
          </el-select>
        </el-form-item>
        <el-form-item label="会议地点">
          <el-input
            v-model="queryParams.location"
            placeholder="请输入会议地点"
            clearable
            style="width: 150px"
          />
        </el-form-item>
        <el-form-item label="时间范围">
          <el-date-picker
            v-model="queryParams.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
          />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="queryParams.status" clearable placeholder="请选择">
            <el-option label="待开始" value="pending" />
            <el-option label="进行中" value="ongoing" />
            <el-option label="已结束" value="completed" />
            <el-option label="已取消" value="cancelled" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">查询</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <el-table
        :data="tableData"
        v-loading="loading"
        border
        style="width: 100%"
        @sort-change="handleSortChange"
      >
        <el-table-column prop="id" label="ID" width="80" fixed />
        <el-table-column prop="title" label="会议主题" width="200" fixed>
          <template #default="scope">
            <el-button type="text" @click="handleView(scope.row)">
              {{ scope.row.title }}
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="meetingType" label="会议类型" width="120">
          <template #default="scope">
            <el-tag :type="getMeetingTypeTag(scope.row.meetingType)">
              {{ getMeetingTypeText(scope.row.meetingType) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="participants" label="参会人员" width="180" show-overflow-tooltip>
          <template #default="scope">
            <span>{{ scope.row.participants.join(', ') }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="location" label="会议地点" width="150" />
        <el-table-column prop="startTime" label="开始时间" width="160" sortable>
          <template #default="scope">
            <span>{{ formatDateTime(scope.row.startTime) }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="endTime" label="结束时间" width="160" sortable>
          <template #default="scope">
            <span>{{ formatDateTime(scope.row.endTime) }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="duration" label="持续时间" width="100">
          <template #default="scope">
            <span>{{ calculateDuration(scope.row) }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="summary" label="会议概要" min-width="200" show-overflow-tooltip />
        <el-table-column prop="status" label="状态" width="100" fixed="right">
          <template #default="scope">
            <el-tag :type="getStatusTag(scope.row.status)">
              {{ getStatusText(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="scope">
            <el-button size="mini" type="text" @click="handleView(scope.row)">
              æŸ¥çœ‹
            </el-button>
            <el-button size="mini" type="text" @click="handleEdit(scope.row)">
              ç¼–辑
            </el-button>
            <el-button
              size="mini"
              type="text"
              @click="handleCopy(scope.row)"
              style="color: #67C23A;"
            >
              å¤åˆ¶
            </el-button>
            <el-button
              size="mini"
              type="text"
              @click="handleDelete(scope.row)"
              style="color: #F56C6C;"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <div class="pagination-container">
        <el-pagination
          :current-page="pagination.currentPage"
          :page-size="pagination.pageSize"
          :total="pagination.total"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </el-card>
    <!-- æŸ¥çœ‹è¯¦æƒ…对话框 -->
    <el-dialog
      :title="`会议详情 - ${currentRecord.title || ''}`"
      :visible.sync="detailDialogVisible"
      width="800px"
      :before-close="handleDetailClose"
    >
      <el-descriptions :column="2" border v-if="currentRecord">
        <el-descriptions-item label="会议主题">{{ currentRecord.title }}</el-descriptions-item>
        <el-descriptions-item label="会议类型">
          <el-tag :type="getMeetingTypeTag(currentRecord.meetingType)">
            {{ getMeetingTypeText(currentRecord.meetingType) }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="参会人员" :span="2">
          <el-tag
            v-for="participant in currentRecord.participants"
            :key="participant"
            type="info"
            size="small"
            style="margin-right: 8px; margin-bottom: 8px;"
          >
            {{ participant }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="会议地点">{{ currentRecord.location }}</el-descriptions-item>
        <el-descriptions-item label="会议状态">
          <el-tag :type="getStatusTag(currentRecord.status)">
            {{ getStatusText(currentRecord.status) }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="开始时间">{{ formatDateTime(currentRecord.startTime) }}</el-descriptions-item>
        <el-descriptions-item label="结束时间">{{ formatDateTime(currentRecord.endTime) }}</el-descriptions-item>
        <el-descriptions-item label="持续时间">{{ calculateDuration(currentRecord) }}</el-descriptions-item>
        <el-descriptions-item label="创建人">{{ currentRecord.creator }}</el-descriptions-item>
        <el-descriptions-item label="会议概要" :span="2">
          {{ currentRecord.summary }}
        </el-descriptions-item>
        <el-descriptions-item label="会议内容" :span="2">
          <div style="white-space: pre-line; max-height: 300px; overflow-y: auto;">
            {{ currentRecord.content }}
          </div>
        </el-descriptions-item>
        <el-descriptions-item label="附件" :span="2" v-if="currentRecord.attachments && currentRecord.attachments.length">
          <div v-for="file in currentRecord.attachments" :key="file.id" class="attachment-item">
            <el-link type="primary" :href="file.url" target="_blank">
              <i class="el-icon-document"></i> {{ file.name }}
            </el-link>
          </div>
        </el-descriptions-item>
      </el-descriptions>
      <span slot="footer">
        <el-button @click="detailDialogVisible = false">关闭</el-button>
        <el-button type="primary" @click="handleEdit(currentRecord)">编辑</el-button>
      </span>
    </el-dialog>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog
      :title="`${isEditing ? '编辑' : '新增'}会议`"
      :visible.sync="editDialogVisible"
      width="700px"
      :before-close="handleEditClose"
    >
      <el-form
        ref="editForm"
        :model="editForm"
        :rules="editRules"
        label-width="100px"
        label-position="left"
      >
        <el-form-item label="会议主题" prop="title">
          <el-input v-model="editForm.title" placeholder="请输入会议主题" />
        </el-form-item>
        <el-form-item label="会议类型" prop="meetingType">
          <el-select v-model="editForm.meetingType" placeholder="请选择会议类型" style="width: 100%">
            <el-option label="科研会议" value="research" />
            <el-option label="日常会议" value="daily" />
            <el-option label="项目会议" value="project" />
            <el-option label="部门会议" value="department" />
            <el-option label="评审会议" value="review" />
          </el-select>
        </el-form-item>
        <el-form-item label="参会人员" prop="participants">
          <el-select
            v-model="editForm.participants"
            multiple
            filterable
            placeholder="请选择参会人员"
            style="width: 100%"
          >
            <el-option
              v-for="user in userList"
              :key="user.id"
              :label="user.name"
              :value="user.name"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="会议地点" prop="location">
          <el-input v-model="editForm.location" placeholder="请输入会议地点" />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="开始时间" prop="startTime">
              <el-date-picker
                v-model="editForm.startTime"
                type="datetime"
                placeholder="选择开始时间"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="结束时间" prop="endTime">
              <el-date-picker
                v-model="editForm.endTime"
                type="datetime"
                placeholder="选择结束时间"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="会议概要" prop="summary">
          <el-input
            v-model="editForm.summary"
            type="textarea"
            :rows="2"
            placeholder="请输入会议概要"
            maxlength="200"
            show-word-limit
          />
        </el-form-item>
        <el-form-item label="会议内容" prop="content">
          <el-input
            v-model="editForm.content"
            type="textarea"
            :rows="6"
            placeholder="请输入会议具体内容"
          />
        </el-form-item>
        <el-form-item label="附件上传">
          <el-upload
            action="#"
            :auto-upload="false"
            :on-change="handleFileChange"
            :file-list="editForm.attachments"
            :limit="5"
            multiple
          >
            <el-button size="small" type="primary">点击上传</el-button>
            <div slot="tip" class="el-upload__tip">支持上传文档、图片等文件,单个文件不超过10MB</div>
          </el-upload>
        </el-form-item>
      </el-form>
      <span slot="footer">
        <el-button @click="handleEditClose">取消</el-button>
        <el-button type="primary" @click="handleSave" :loading="saveLoading">
          {{ isEditing ? '保存' : '新增' }}
        </el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
export default {
  name: 'MeetingManagement',
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        meetingType: '',
        location: '',
        dateRange: [],
        status: ''
      },
      // åˆ†é¡µå‚æ•°
      pagination: {
        currentPage: 1,
        pageSize: 10,
        total: 0
      },
      // åŠ è½½çŠ¶æ€
      loading: false,
      saveLoading: false,
      // å¯¹è¯æ¡†æ˜¾ç¤ºçŠ¶æ€
      detailDialogVisible: false,
      editDialogVisible: false,
      // å½“前操作记录
      currentRecord: {},
      // ç¼–辑状态
      isEditing: false,
      // è¡¨æ ¼æ•°æ®
      tableData: [],
      // ç”¨æˆ·åˆ—表(用于选择参会人员)
      userList: [
        { id: 1, name: '张三' },
        { id: 2, name: '李四' },
        { id: 3, name: '王五' },
        { id: 4, name: '赵六' },
        { id: 5, name: '钱七' },
        { id: 6, name: '孙八' },
        { id: 7, name: '周九' },
        { id: 8, name: '吴十' }
      ],
      // ç¼–辑表单数据
      editForm: {
        title: '',
        meetingType: '',
        participants: [],
        location: '',
        startTime: '',
        endTime: '',
        summary: '',
        content: '',
        attachments: []
      },
      // è¡¨å•验证规则
      editRules: {
        title: [{ required: true, message: '请输入会议主题', trigger: 'blur' }],
        meetingType: [{ required: true, message: '请选择会议类型', trigger: 'change' }],
        participants: [{ required: true, message: '请选择参会人员', trigger: 'change' }],
        location: [{ required: true, message: '请输入会议地点', trigger: 'blur' }],
        startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
        endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
        summary: [{ required: true, message: '请输入会议概要', trigger: 'blur' }],
        content: [{ required: true, message: '请输入会议内容', trigger: 'blur' }]
      }
    }
  },
  mounted() {
    this.loadData()
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true
      try {
        // æ¨¡æ‹ŸAPI调用
        await new Promise(resolve => setTimeout(resolve, 500))
        // ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
        this.tableData = this.generateMockData()
        this.pagination.total = this.tableData.length
      } catch (error) {
        console.error('加载数据失败:', error)
        this.$message.error('数据加载失败')
      } finally {
        this.loading = false
      }
    },
    // ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
    generateMockData() {
      const meetingTypes = ['research', 'daily', 'project', 'department', 'review']
      const statuses = ['pending', 'ongoing', 'completed', 'cancelled']
      const participantsPool = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
      return Array.from({ length: 10 }, (_, index) => {
        const participantCount = Math.floor(Math.random() * 5) + 2
        const participants = []
        for (let i = 0; i < participantCount; i++) {
          const randomIndex = Math.floor(Math.random() * participantsPool.length)
          participants.push(participantsPool[randomIndex])
        }
        // åŽ»é‡
        const uniqueParticipants = [...new Set(participants)]
        const startTime = new Date()
        startTime.setDate(startTime.getDate() + Math.floor(Math.random() * 30) - 15)
        startTime.setHours(9 + Math.floor(Math.random() * 8), Math.floor(Math.random() * 4) * 15, 0)
        const endTime = new Date(startTime)
        endTime.setHours(startTime.getHours() + Math.floor(Math.random() * 3) + 1)
        return {
          id: index + 1,
          title: `关于${['科研项目', '日常工作', '技术评审', '部门协调'][Math.floor(Math.random() * 4)]}的会议`,
          meetingType: meetingTypes[Math.floor(Math.random() * meetingTypes.length)],
          participants: uniqueParticipants,
          location: ['第一会议室', '第二会议室', '第三会议室', '线上会议'][Math.floor(Math.random() * 4)],
          startTime: startTime.toISOString(),
          endTime: endTime.toISOString(),
          summary: `本次会议主要讨论${['项目进展', '技术难题', '工作计划', '问题协调'][Math.floor(Math.random() * 4)]}等相关事宜`,
          content: `会议详细内容:\n1. è®®é¢˜ä¸€è®¨è®º\n2. è®®é¢˜äºŒåˆ†æž\n3. ä¸‹ä¸€æ­¥å·¥ä½œè®¡åˆ’\n4. ä»»åŠ¡åˆ†é…`,
          status: statuses[Math.floor(Math.random() * statuses.length)],
          creator: '系统管理员',
          attachments: []
        }
      })
    },
    // èŽ·å–ä¼šè®®ç±»åž‹æ ‡ç­¾æ ·å¼
    getMeetingTypeTag(type) {
      const typeMap = {
        research: 'primary',
        daily: 'success',
        project: 'warning',
        department: 'info',
        review: 'danger'
      }
      return typeMap[type] || 'info'
    },
    // èŽ·å–ä¼šè®®ç±»åž‹æ–‡æœ¬
    getMeetingTypeText(type) {
      const textMap = {
        research: '科研会议',
        daily: '日常会议',
        project: '项目会议',
        department: '部门会议',
        review: '评审会议'
      }
      return textMap[type] || type
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾æ ·å¼
    getStatusTag(status) {
      const statusMap = {
        pending: 'primary',
        ongoing: 'success',
        completed: 'info',
        cancelled: 'danger'
      }
      return statusMap[status] || 'info'
    },
    // èŽ·å–çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      const textMap = {
        pending: '待开始',
        ongoing: '进行中',
        completed: '已结束',
        cancelled: '已取消'
      }
      return textMap[status] || status
    },
    // æ ¼å¼åŒ–日期时间
    formatDateTime(dateTime) {
      if (!dateTime) return ''
      const date = new Date(dateTime)
      return date.toLocaleString('zh-CN')
    },
    // è®¡ç®—会议持续时间
    calculateDuration(record) {
      if (!record.startTime || !record.endTime) return ''
      const start = new Date(record.startTime)
      const end = new Date(record.endTime)
      const duration = (end - start) / (1000 * 60) // åˆ†é’Ÿæ•°
      if (duration < 60) {
        return `${Math.round(duration)}分钟`
      } else {
        const hours = Math.floor(duration / 60)
        const minutes = Math.round(duration % 60)
        return minutes > 0 ? `${hours}小时${minutes}分钟` : `${hours}小时`
      }
    },
    // æŸ¥è¯¢å¤„理
    handleQuery() {
      this.pagination.currentPage = 1
      this.loadData()
    },
    // é‡ç½®æŸ¥è¯¢
    handleReset() {
      this.queryParams = {
        meetingType: '',
        location: '',
        dateRange: [],
        status: ''
      }
      this.pagination.currentPage = 1
      this.loadData()
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleView(record) {
      this.currentRecord = { ...record }
      this.detailDialogVisible = true
    },
    // æ–°å¢žè®°å½•
    handleAdd() {
      this.isEditing = false
      this.editForm = this.getDefaultFormData()
      this.editDialogVisible = true
      this.$nextTick(() => {
        this.$refs.editForm && this.$refs.editForm.clearValidate()
      })
    },
    // ç¼–辑记录
    handleEdit(record) {
      this.isEditing = true
      this.currentRecord = record
      this.editForm = { ...record }
      this.editDialogVisible = true
      this.detailDialogVisible = false
      this.$nextTick(() => {
        this.$refs.editForm && this.$refs.editForm.clearValidate()
      })
    },
    // å¤åˆ¶è®°å½•
    handleCopy(record) {
      this.isEditing = false
      const copiedRecord = { ...record }
      delete copiedRecord.id
      copiedRecord.title = copiedRecord.title + '(复制)'
      this.editForm = copiedRecord
      this.editDialogVisible = true
      this.$nextTick(() => {
        this.$refs.editForm && this.$refs.editForm.clearValidate()
      })
    },
    // åˆ é™¤è®°å½•
    handleDelete(record) {
      this.$confirm('确定要删除这条会议记录吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        // æ¨¡æ‹Ÿåˆ é™¤æ“ä½œ
        this.tableData = this.tableData.filter(item => item.id !== record.id)
        this.pagination.total = this.tableData.length
        this.$message.success('删除成功')
      }).catch(() => {})
    },
    // ä¿å­˜è®°å½•
    async handleSave() {
      try {
        const valid = await this.$refs.editForm.validate()
        if (!valid) return
        this.saveLoading = true
        // æ¨¡æ‹ŸAPI调用
        await new Promise(resolve => setTimeout(resolve, 1000))
        this.$message.success(this.isEditing ? '保存成功' : '新增成功')
        this.editDialogVisible = false
        this.loadData()
      } catch (error) {
        console.error('保存失败:', error)
        this.$message.error('操作失败')
      } finally {
        this.saveLoading = false
      }
    },
    // æ–‡ä»¶ä¸Šä¼ å¤„理
    handleFileChange(file, fileList) {
      this.editForm.attachments = fileList
    },
    // å…³é—­è¯¦æƒ…对话框
    handleDetailClose() {
      this.detailDialogVisible = false
      this.currentRecord = {}
    },
    // å…³é—­ç¼–辑对话框
    handleEditClose() {
      this.editDialogVisible = false
      this.currentRecord = {}
      this.$nextTick(() => {
        this.$refs.editForm && this.$refs.editForm.clearValidate()
      })
    },
    // å¯¼å‡ºæ•°æ®
    exportData() {
      this.$message.success('导出功能开发中')
    },
    // åˆ†é¡µå¤§å°å˜åŒ–
    handleSizeChange(size) {
      this.pagination.pageSize = size
      this.pagination.currentPage = 1
      this.loadData()
    },
    // å½“前页变化
    handleCurrentChange(page) {
      this.pagination.currentPage = page
      this.loadData()
    },
    // æŽ’序变化
    handleSortChange(sort) {
      console.log('排序变化:', sort)
    },
    // èŽ·å–é»˜è®¤è¡¨å•æ•°æ®
    getDefaultFormData() {
      return {
        title: '',
        meetingType: '',
        participants: [],
        location: '',
        startTime: '',
        endTime: '',
        summary: '',
        content: '',
        attachments: []
      }
    }
  }
}
</script>
<style scoped>
.meeting-management {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.filter-card {
  margin-bottom: 20px;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
.attachment-item {
  margin-bottom: 8px;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .page-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
  .header-actions {
    width: 100%;
    justify-content: space-between;
  }
}
</style>
src/views/OfficeRelated/engage/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,691 @@
<template>
  <div class="training-management">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <div class="page-header">
      <h2>进修管理</h2>
      <div class="header-actions">
        <el-button type="primary" icon="el-icon-plus" @click="handleAdd">
          æ–°å¢žè¿›ä¿®è®°å½•
        </el-button>
        <el-button icon="el-icon-download" @click="exportData">
          å¯¼å‡ºæ•°æ®
        </el-button>
      </div>
    </div>
    <!-- æœç´¢ç­›é€‰åŒºåŸŸ -->
    <el-card class="filter-card">
      <el-form :model="queryParams" inline>
        <el-form-item label="姓名">
          <el-input
            v-model="queryParams.name"
            placeholder="请输入姓名"
            clearable
            style="width: 120px"
          />
        </el-form-item>
        <el-form-item label="进修类型">
          <el-select v-model="queryParams.trainingType" clearable placeholder="请选择">
            <el-option label="专业技术进修" value="professional" />
            <el-option label="管理能力进修" value="management" />
            <el-option label="学历提升进修" value="education" />
            <el-option label="技能培训" value="skill" />
          </el-select>
        </el-form-item>
        <el-form-item label="技术职称">
          <el-input
            v-model="queryParams.technicalTitle"
            placeholder="请输入技术职称"
            clearable
            style="width: 140px"
          />
        </el-form-item>
        <el-form-item label="进修时间">
          <el-date-picker
            v-model="queryParams.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">查询</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <el-table
        :data="tableData"
        v-loading="loading"
        border
        style="width: 100%"
        @sort-change="handleSortChange"
      >
        <el-table-column prop="id" label="ID" width="80" fixed />
        <el-table-column prop="name" label="姓名" width="100" fixed>
          <template #default="scope">
            <el-button type="text" @click="handleView(scope.row)">
              {{ scope.row.name }}
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="gender" label="性别" width="80">
          <template #default="scope">
            <el-tag :type="scope.row.gender === '男' ? 'primary' : 'danger'" size="small">
              {{ scope.row.gender }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="age" label="年龄" width="80" sortable />
        <el-table-column prop="education" label="学历" width="100" />
        <el-table-column prop="trainingType" label="进修类型" width="120">
          <template #default="scope">
            <el-tag :type="getTrainingTypeTag(scope.row.trainingType)">
              {{ getTrainingTypeText(scope.row.trainingType) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="idCard" label="身份证号" width="180" show-overflow-tooltip />
        <el-table-column prop="graduateSchool" label="毕业院校" width="150" show-overflow-tooltip />
        <el-table-column prop="workUnit" label="所在单位" width="150" show-overflow-tooltip />
        <el-table-column prop="technicalTitle" label="技术职称" width="120" />
        <el-table-column prop="professionalField" label="从事专业" width="120" />
        <el-table-column prop="workYears" label="工作年限" width="100" sortable />
        <el-table-column prop="trainingStartDate" label="进修开始时间" width="120" />
        <el-table-column prop="trainingEndDate" label="进修结束时间" width="120" />
        <el-table-column prop="trainingMajor" label="进修专业" width="120" />
        <el-table-column prop="status" label="状态" width="100" fixed="right">
          <template #default="scope">
            <el-tag :type="getStatusTag(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="180" fixed="right">
          <template #default="scope">
            <el-button size="mini" type="text" @click="handleView(scope.row)">
              æŸ¥çœ‹
            </el-button>
            <el-button size="mini" type="text" @click="handleEdit(scope.row)">
              ç¼–辑
            </el-button>
            <el-button
              size="mini"
              type="text"
              @click="handleCopy(scope.row)"
              style="color: #67C23A;"
            >
              å¤åˆ¶
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <div class="pagination-container">
        <el-pagination
          :current-page="pagination.currentPage"
          :page-size="pagination.pageSize"
          :total="pagination.total"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </el-card>
    <!-- æŸ¥çœ‹è¯¦æƒ…对话框 -->
    <el-dialog
      :title="`进修详情 - ${currentRecord.name || ''}`"
      :visible.sync="detailDialogVisible"
      width="900px"
      :before-close="handleDetailClose"
    >
      <el-descriptions :column="2" border v-if="currentRecord">
        <el-descriptions-item label="姓名">{{ currentRecord.name }}</el-descriptions-item>
        <el-descriptions-item label="性别">{{ currentRecord.gender }}</el-descriptions-item>
        <el-descriptions-item label="年龄">{{ currentRecord.age }}</el-descriptions-item>
        <el-descriptions-item label="学历">{{ currentRecord.education }}</el-descriptions-item>
        <el-descriptions-item label="进修类型">
          <el-tag :type="getTrainingTypeTag(currentRecord.trainingType)">
            {{ getTrainingTypeText(currentRecord.trainingType) }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="身份证号">{{ currentRecord.idCard }}</el-descriptions-item>
        <el-descriptions-item label="毕业院校">{{ currentRecord.graduateSchool }}</el-descriptions-item>
        <el-descriptions-item label="所在单位">{{ currentRecord.workUnit }}</el-descriptions-item>
        <el-descriptions-item label="技术职称">{{ currentRecord.technicalTitle }}</el-descriptions-item>
        <el-descriptions-item label="从事专业">{{ currentRecord.professionalField }}</el-descriptions-item>
        <el-descriptions-item label="工作年限">{{ currentRecord.workYears }}å¹´</el-descriptions-item>
        <el-descriptions-item label="进修目标">{{ currentRecord.trainingObjective }}</el-descriptions-item>
        <el-descriptions-item label="进修开始时间">{{ currentRecord.trainingStartDate }}</el-descriptions-item>
        <el-descriptions-item label="进修结束时间">{{ currentRecord.trainingEndDate }}</el-descriptions-item>
        <el-descriptions-item label="进修专业">{{ currentRecord.trainingMajor }}</el-descriptions-item>
        <el-descriptions-item label="从事工作情况" :span="2">
          {{ currentRecord.workSituation }}
        </el-descriptions-item>
        <el-descriptions-item label="进修科目及目的" :span="2">
          {{ currentRecord.trainingSubject }}
        </el-descriptions-item>
        <el-descriptions-item label="主要学历" :span="2">
          <div style="white-space: pre-line;">{{ currentRecord.mainEducation }}</div>
        </el-descriptions-item>
        <el-descriptions-item label="主要工作经历" :span="2">
          <div style="white-space: pre-line;">{{ currentRecord.workExperience }}</div>
        </el-descriptions-item>
      </el-descriptions>
      <span slot="footer">
        <el-button @click="detailDialogVisible = false">关闭</el-button>
        <el-button type="primary" @click="handleEdit(currentRecord)">编辑</el-button>
      </span>
    </el-dialog>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog
      :title="`${isEditing ? '编辑' : '新增'}进修记录`"
      :visible.sync="editDialogVisible"
      width="80vw"
      :before-close="handleEditClose"
    >
      <el-form
        ref="editForm"
        :model="editForm"
        :rules="editRules"
        label-width="120px"
        label-position="left"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="姓名" prop="name">
              <el-input v-model="editForm.name" placeholder="请输入姓名" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="性别" prop="gender">
              <el-radio-group v-model="editForm.gender">
                <el-radio label="男">男</el-radio>
                <el-radio label="女">女</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="年龄" prop="age">
              <el-input-number
                v-model="editForm.age"
                :min="18"
                :max="65"
                controls-position="right"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="学历" prop="education">
              <el-select v-model="editForm.education" placeholder="请选择学历" style="width: 100%">
                <el-option label="博士" value="博士" />
                <el-option label="硕士" value="硕士" />
                <el-option label="本科" value="本科" />
                <el-option label="大专" value="大专" />
                <el-option label="中专" value="中专" />
                <el-option label="高中" value="高中" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="进修类型" prop="trainingType">
          <el-select v-model="editForm.trainingType" placeholder="请选择进修类型" style="width: 100%">
            <el-option label="专业技术进修" value="professional" />
            <el-option label="管理能力进修" value="management" />
            <el-option label="学历提升进修" value="education" />
            <el-option label="技能培训" value="skill" />
          </el-select>
        </el-form-item>
        <el-form-item label="身份证号" prop="idCard">
          <el-input v-model="editForm.idCard" placeholder="请输入身份证号" />
        </el-form-item>
        <el-form-item label="毕业院校" prop="graduateSchool">
          <el-input v-model="editForm.graduateSchool" placeholder="请输入毕业院校" />
        </el-form-item>
        <el-form-item label="所在单位" prop="workUnit">
          <el-input v-model="editForm.workUnit" placeholder="请输入所在单位" />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="技术职称" prop="technicalTitle">
              <el-input v-model="editForm.technicalTitle" placeholder="请输入技术职称" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="从事专业" prop="professionalField">
              <el-input v-model="editForm.professionalField" placeholder="请输入从事专业" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="工作年限" prop="workYears">
          <el-input-number
            v-model="editForm.workYears"
            :min="0"
            :max="50"
            controls-position="right"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="进修目标" prop="trainingObjective">
          <el-input
            v-model="editForm.trainingObjective"
            type="textarea"
            :rows="2"
            placeholder="请输入进修目标"
          />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="进修开始时间" prop="trainingStartDate">
              <el-date-picker
                v-model="editForm.trainingStartDate"
                type="date"
                placeholder="选择开始日期"
                value-format="yyyy-MM-dd"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="进修结束时间" prop="trainingEndDate">
              <el-date-picker
                v-model="editForm.trainingEndDate"
                type="date"
                placeholder="选择结束日期"
                value-format="yyyy-MM-dd"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="进修专业" prop="trainingMajor">
          <el-input v-model="editForm.trainingMajor" placeholder="请输入进修专业" />
        </el-form-item>
        <el-form-item label="从事工作情况" prop="workSituation">
          <el-input
            v-model="editForm.workSituation"
            type="textarea"
            :rows="3"
            placeholder="请输入从事工作情况"
          />
        </el-form-item>
        <el-form-item label="进修科目及目的" prop="trainingSubject">
          <el-input
            v-model="editForm.trainingSubject"
            type="textarea"
            :rows="3"
            placeholder="请输入本次申请进修何种科目及目的要求"
          />
        </el-form-item>
        <el-form-item label="主要学历" prop="mainEducation">
          <el-input
            v-model="editForm.mainEducation"
            type="textarea"
            :rows="3"
            placeholder="请输入主要学历(每行一条)"
          />
        </el-form-item>
        <el-form-item label="主要工作经历" prop="workExperience">
          <el-input
            v-model="editForm.workExperience"
            type="textarea"
            :rows="3"
            placeholder="请输入主要工作经历(每行一条)"
          />
        </el-form-item>
      </el-form>
      <span slot="footer">
        <el-button @click="handleEditClose">取消</el-button>
        <el-button type="primary" @click="handleSave" :loading="saveLoading">
          {{ isEditing ? '保存' : '新增' }}
        </el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
export default {
  name: 'TrainingManagement',
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        name: '',
        trainingType: '',
        technicalTitle: '',
        dateRange: []
      },
      // åˆ†é¡µå‚æ•°
      pagination: {
        currentPage: 1,
        pageSize: 10,
        total: 0
      },
      // åŠ è½½çŠ¶æ€
      loading: false,
      saveLoading: false,
      // å¯¹è¯æ¡†æ˜¾ç¤ºçŠ¶æ€
      detailDialogVisible: false,
      editDialogVisible: false,
      // å½“前操作记录
      currentRecord: {},
      // ç¼–辑状态
      isEditing: false,
      // è¡¨æ ¼æ•°æ®
      tableData: [],
      // ç¼–辑表单数据
      editForm: {
        name: '',
        gender: '男',
        age: 25,
        education: '',
        trainingType: '',
        idCard: '',
        graduateSchool: '',
        workUnit: '',
        technicalTitle: '',
        professionalField: '',
        workYears: 0,
        trainingObjective: '',
        trainingStartDate: '',
        trainingEndDate: '',
        trainingMajor: '',
        workSituation: '',
        trainingSubject: '',
        mainEducation: '',
        workExperience: ''
      },
      // è¡¨å•验证规则
      editRules: {
        name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
        gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
        age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
        education: [{ required: true, message: '请选择学历', trigger: 'change' }],
        trainingType: [{ required: true, message: '请选择进修类型', trigger: 'change' }],
        idCard: [{ required: true, message: '请输入身份证号', trigger: 'blur' }]
      }
    }
  },
  mounted() {
    this.loadData()
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true
      try {
        // æ¨¡æ‹ŸAPI调用
        await new Promise(resolve => setTimeout(resolve, 500))
        // ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
        this.tableData = this.generateMockData()
        this.pagination.total = this.tableData.length
      } catch (error) {
        console.error('加载数据失败:', error)
        this.$message.error('数据加载失败')
      } finally {
        this.loading = false
      }
    },
    // ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
    generateMockData() {
      const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
      const trainingTypes = ['professional', 'management', 'education', 'skill']
      const statuses = ['进行中', '已完成', '待审核', '已取消']
      return Array.from({ length: 10 }, (_, index) => ({
        id: index + 1,
        name: names[Math.floor(Math.random() * names.length)],
        gender: Math.random() > 0.5 ? '男' : '女',
        age: 25 + Math.floor(Math.random() * 20),
        education: ['博士', '硕士', '本科', '大专'][Math.floor(Math.random() * 4)],
        trainingType: trainingTypes[Math.floor(Math.random() * trainingTypes.length)],
        idCard: '11010119900101' + (1000 + index).toString().slice(-4),
        graduateSchool: ['北京大学', '清华大学', '复旦大学', '上海交通大学'][Math.floor(Math.random() * 4)],
        workUnit: ['北京协和医院', '上海瑞金医院', '广州中山医院', '武汉同济医院'][Math.floor(Math.random() * 4)],
        technicalTitle: ['主任医师', '副主任医师', '主治医师', '住院医师'][Math.floor(Math.random() * 4)],
        professionalField: ['心血管内科', '神经外科', '儿科', '妇产科'][Math.floor(Math.random() * 4)],
        workYears: 5 + Math.floor(Math.random() * 20),
        trainingObjective: '提升专业技术和临床能力',
        trainingStartDate: '2024-' + (Math.floor(Math.random() * 12) + 1).toString().padStart(2, '0') + '-01',
        trainingEndDate: '2024-' + (Math.floor(Math.random() * 12) + 1).toString().padStart(2, '0') + '-28',
        trainingMajor: '临床医学',
        workSituation: '在临床一线工作,负责患者诊疗',
        trainingSubject: '高级心血管介入治疗技术',
        mainEducation: '2005-2009 åŒ—京大学医学部 ä¸´åºŠåŒ»å­¦æœ¬ç§‘\n2009-2012 åŒ—京大学医学部 ç¡•士',
        workExperience: '2012-2015 åŒ—京协和医院 ä½é™¢åŒ»å¸ˆ\n2015-2020 åŒ—京协和医院 ä¸»æ²»åŒ»å¸ˆ',
        status: statuses[Math.floor(Math.random() * statuses.length)]
      }))
    },
    // èŽ·å–è¿›ä¿®ç±»åž‹æ ‡ç­¾æ ·å¼
    getTrainingTypeTag(type) {
      const typeMap = {
        professional: 'primary',
        management: 'success',
        education: 'warning',
        skill: 'info'
      }
      return typeMap[type] || 'info'
    },
    // èŽ·å–è¿›ä¿®ç±»åž‹æ–‡æœ¬
    getTrainingTypeText(type) {
      const textMap = {
        professional: '专业技术进修',
        management: '管理能力进修',
        education: '学历提升进修',
        skill: '技能培训'
      }
      return textMap[type] || type
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾æ ·å¼
    getStatusTag(status) {
      const statusMap = {
        '进行中': 'primary',
        '已完成': 'success',
        '待审核': 'warning',
        '已取消': 'danger'
      }
      return statusMap[status] || 'info'
    },
    // æŸ¥è¯¢å¤„理
    handleQuery() {
      this.pagination.currentPage = 1
      this.loadData()
    },
    // é‡ç½®æŸ¥è¯¢
    handleReset() {
      this.queryParams = {
        name: '',
        trainingType: '',
        technicalTitle: '',
        dateRange: []
      }
      this.pagination.currentPage = 1
      this.loadData()
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleView(record) {
      this.currentRecord = { ...record }
      this.detailDialogVisible = true
    },
    // æ–°å¢žè®°å½•
    handleAdd() {
      this.isEditing = false
      this.editForm = this.getDefaultFormData()
      this.editDialogVisible = true
      this.$nextTick(() => {
        this.$refs.editForm && this.$refs.editForm.clearValidate()
      })
    },
    // ç¼–辑记录
    handleEdit(record) {
      this.isEditing = true
      this.currentRecord = record
      this.editForm = { ...record }
      this.editDialogVisible = true
      this.detailDialogVisible = false
      this.$nextTick(() => {
        this.$refs.editForm && this.$refs.editForm.clearValidate()
      })
    },
    // å¤åˆ¶è®°å½•
    handleCopy(record) {
      this.isEditing = false
      const copiedRecord = { ...record }
      delete copiedRecord.id
      copiedRecord.name = copiedRecord.name + '(复制)'
      this.editForm = copiedRecord
      this.editDialogVisible = true
      this.$nextTick(() => {
        this.$refs.editForm && this.$refs.editForm.clearValidate()
      })
    },
    // ä¿å­˜è®°å½•
    async handleSave() {
      try {
        const valid = await this.$refs.editForm.validate()
        if (!valid) return
        this.saveLoading = true
        // æ¨¡æ‹ŸAPI调用
        await new Promise(resolve => setTimeout(resolve, 1000))
        this.$message.success(this.isEditing ? '保存成功' : '新增成功')
        this.editDialogVisible = false
        this.loadData()
      } catch (error) {
        console.error('保存失败:', error)
        this.$message.error('操作失败')
      } finally {
        this.saveLoading = false
      }
    },
    // å…³é—­è¯¦æƒ…对话框
    handleDetailClose() {
      this.detailDialogVisible = false
      this.currentRecord = {}
    },
    // å…³é—­ç¼–辑对话框
    handleEditClose() {
      this.editDialogVisible = false
      this.currentRecord = {}
      this.$nextTick(() => {
        this.$refs.editForm && this.$refs.editForm.clearValidate()
      })
    },
    // å¯¼å‡ºæ•°æ®
    exportData() {
      this.$message.success('导出功能开发中')
    },
    // åˆ†é¡µå¤§å°å˜åŒ–
    handleSizeChange(size) {
      this.pagination.pageSize = size
      this.pagination.currentPage = 1
      this.loadData()
    },
    // å½“前页变化
    handleCurrentChange(page) {
      this.pagination.currentPage = page
      this.loadData()
    },
    // æŽ’序变化
    handleSortChange(sort) {
      console.log('排序变化:', sort)
      // è¿™é‡Œå¯ä»¥æ·»åŠ æŽ’åºé€»è¾‘
    },
    // èŽ·å–é»˜è®¤è¡¨å•æ•°æ®
    getDefaultFormData() {
      return {
        name: '',
        gender: '男',
        age: 25,
        education: '',
        trainingType: '',
        idCard: '',
        graduateSchool: '',
        workUnit: '',
        technicalTitle: '',
        professionalField: '',
        workYears: 0,
        trainingObjective: '',
        trainingStartDate: '',
        trainingEndDate: '',
        trainingMajor: '',
        workSituation: '',
        trainingSubject: '',
        mainEducation: '',
        workExperience: ''
      }
    }
  }
}
</script>
<style scoped>
.training-management {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.filter-card {
  margin-bottom: 20px;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .page-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
  .header-actions {
    width: 100%;
    justify-content: space-between;
  }
}
</style>