| | |
| | | <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> |
| | |
| | | <!-- éé¡¹å¡ --> |
| | | <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" |
| | |
| | | :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" |
| | |
| | | </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, |
| | |
| | | }, |
| | | 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, |
| | |
| | | }, |
| | | 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> |
| | |
| | | <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> |
| | | |
| | |
| | | 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 |
| | | } |
| | | } |
| | | } |
| | |
| | | <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> |
| | |
| | | size="small" |
| | | @click="exportReport" |
| | | icon="el-icon-download" |
| | | type="primary" |
| | | > |
| | | å¯¼åºæ¥è¡¨ |
| | | </el-button> |
| | |
| | | |
| | | <!-- ç»è®¡æ¦è§ --> |
| | | <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"> |
| | |
| | | </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"> |
| | |
| | | </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"> |
| | |
| | | </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"> |
| | |
| | | </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> |
| | |
| | | detailedStats: [], |
| | | abnormalRecords: [], |
| | | trendChart: null, |
| | | distributionChart: null |
| | | distributionChart: null, |
| | | tableExpanded: true |
| | | } |
| | | }, |
| | | mounted() { |
| | |
| | | }) |
| | | }, |
| | | beforeDestroy() { |
| | | // 鿝å¾è¡¨å®ä¾ |
| | | // 鿝å¾è¡¨å®ä¾[3](@ref) |
| | | if (this.trendChart) { |
| | | this.trendChart.dispose() |
| | | } |
| | |
| | | }, |
| | | methods: { |
| | | initData() { |
| | | // åå§åæ¦è§æ°æ® |
| | | // åå§åæ°æ®é»è¾ä¿æä¸å |
| | | this.overview = { |
| | | totalDays: this.stats.totalDays || 22, |
| | | presentDays: this.stats.presentDays || 20, |
| | |
| | | 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 }, |
| | |
| | | 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: 'å·²å¤ç' }, |
| | |
| | | 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 |
| | | } |
| | | } |
| | | ], |
| | |
| | | { |
| | | 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)' |
| | | } |
| | | } |
| | | }, |
| | | { |
| | |
| | | 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) |
| | | }, |
| | |
| | | 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: [ |
| | | { |
| | |
| | | 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' } }, |
| | |
| | | { 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', () => { |
| | |
| | | }) |
| | | }, |
| | | |
| | | 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': |
| | |
| | | default: |
| | | data = this.getMonthlyData() |
| | | } |
| | | |
| | | this.updateCharts(data) |
| | | }, |
| | | |
| | | getMonthlyData() { |
| | | // 模ææåº¦æ°æ® |
| | | return { |
| | | xAxis: ['10æ', '11æ', '12æ'], |
| | | attendance: [22, 19, 20], |
| | |
| | | }, |
| | | |
| | | getQuarterlyData() { |
| | | // 模æå£åº¦æ°æ® |
| | | return { |
| | | xAxis: ['Q1', 'Q2', 'Q3', 'Q4'], |
| | | attendance: [65, 62, 58, 61], |
| | |
| | | }, |
| | | |
| | | getYearlyData() { |
| | | // 模æå¹´åº¦æ°æ® |
| | | return { |
| | | xAxis: ['2022', '2023', '2024'], |
| | | attendance: [240, 248, 252], |
| | |
| | | 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 }) |
| | | } |
| | | }, |
| | | |
| | |
| | | |
| | | viewAbnormalDetail(record) { |
| | | this.$message.info(`æ¥çå¼å¸¸è®°å½: ${record.date} - ${record.type}`) |
| | | // è¿éå¯ä»¥æå¼è¯¦æ
å¯¹è¯æ¡ |
| | | }, |
| | | |
| | | exportReport() { |
| | | this.$message.success('æ¥è¡¨å¯¼åºåè½å¼åä¸') |
| | | // è¿éå¯ä»¥å®ç°å¯¼åºPDFæExcelåè½ |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | <style scoped> |
| | | .personal-attendance-report { |
| | | padding: 20px; |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | padding: 24px; |
| | | background: #f8f9fa; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .report-header { |
| | |
| | | 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 { |
| | |
| | | } |
| | | |
| | | .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 { |
| | |
| | | } |
| | | |
| | | .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 { |
| | |
| | | } |
| | | |
| | | .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 { |
| | |
| | | } |
| | | |
| | | .chart-container { |
| | | height: 250px; |
| | | height: 200px; |
| | | } |
| | | } |
| | | |
| | |
| | | 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> |
| | |
| | | // å¨ç¶ç»ä»¶ 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, |
| | |
| | | startCity: 'å京', |
| | | endCity: '䏿µ·', |
| | | startDate: '2024-12-05', |
| | | endDate: '2024-12-08', |
| | | endDate: '2024-12-07', // è°æ´ä¸º7å·ç»æ |
| | | distance: 1200, |
| | | purpose: '客æ·ä¼è®®', |
| | | status: 'completed' |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |