From 1b736033f6d01b774d58b4c2d7cd2ce8607a44fa Mon Sep 17 00:00:00 2001
From: WXL <wl_5969728@163.com>
Date: 星期日, 28 十二月 2025 20:28:46 +0800
Subject: [PATCH] 页面完善
---
src/views/OfficeRelated/conference/index.vue | 728 ++++++++++++++
src/views/OfficeRelated/checkingIn/checkingInInfo.vue | 213 +--
src/views/OfficeRelated/checkingIn/components/PersonalAttendanceReport.vue | 680 ++++++++++---
src/views/OfficeRelated/checkingIn/mockData.js | 169 --
src/views/OfficeRelated/checkingIn/components/AttendanceCalendar.vue | 542 ---------
src/views/OfficeRelated/engage/index.vue | 691 +++++++++++++
6 files changed, 2,120 insertions(+), 903 deletions(-)
diff --git a/src/views/OfficeRelated/checkingIn/checkingInInfo.vue b/src/views/OfficeRelated/checkingIn/checkingInInfo.vue
index f0c23c0..2ff3344 100644
--- a/src/views/OfficeRelated/checkingIn/checkingInInfo.vue
+++ b/src/views/OfficeRelated/checkingIn/checkingInInfo.vue
@@ -4,12 +4,18 @@
<el-card class="employee-info-card">
<div class="employee-header">
<div class="employee-basic">
- <el-avatar :size="60" :src="employeeInfo.avatar" class="employee-avatar">
+ <el-avatar
+ :size="60"
+ :src="employeeInfo.avatar"
+ class="employee-avatar"
+ >
{{ employeeInfo.name.charAt(0) }}
</el-avatar>
<div class="employee-details">
<h3>{{ employeeInfo.name }}</h3>
- <p class="employee-department">{{ employeeInfo.department }} 路 {{ employeeInfo.position }}</p>
+ <p class="employee-department">
+ {{ employeeInfo.department }} 路 {{ employeeInfo.position }}
+ </p>
<p class="employee-contact">
<span>宸ュ彿: {{ employeeInfo.employeeId }}</span>
<span>鐢佃瘽: {{ employeeInfo.phone }}</span>
@@ -36,13 +42,6 @@
<!-- 閫夐」鍗� -->
<el-card>
<el-tabs v-model="activeTab">
- <el-tab-pane label="鏃ュ巻瑙嗗浘" name="calendar">
- <attendance-calendar
- :attendance-data="attendanceData"
- :business-trip-data="businessTripData"
- />
- </el-tab-pane>
-
<el-tab-pane label="鍑哄嫟璁板綍" name="attendanceList">
<personal-attendance-table
:data="attendanceData"
@@ -56,7 +55,12 @@
:loading="loading"
/>
</el-tab-pane>
-
+ <el-tab-pane label="鏃ュ巻瑙嗗浘" name="calendar">
+ <attendance-calendar
+ :attendance-data="attendanceData"
+ :business-trip-data="businessTripData"
+ />
+ </el-tab-pane>
<el-tab-pane label="缁熻鎶ヨ〃" name="report">
<personal-attendance-report
:stats="employeeStats"
@@ -69,15 +73,13 @@
</template>
<script>
- import { generateMockData } from './mockData'
-
-import AttendanceCalendar from './components/AttendanceCalendar.vue'
-import PersonalAttendanceTable from './components/PersonalAttendanceTable.vue'
-import PersonBusiness from './components/PersonBusiness.vue'
-import PersonalAttendanceReport from './components/PersonalAttendanceReport.vue'
+import AttendanceCalendar from "./components/AttendanceCalendar.vue";
+import PersonalAttendanceTable from "./components/PersonalAttendanceTable.vue";
+import PersonBusiness from "./components/PersonBusiness.vue";
+import PersonalAttendanceReport from "./components/PersonalAttendanceReport.vue";
export default {
- name: 'AttendanceDetail',
+ name: "AttendanceDetail",
components: {
AttendanceCalendar,
PersonalAttendanceTable,
@@ -86,15 +88,15 @@
},
data() {
return {
- activeTab: 'calendar',
+ activeTab: "calendar",
loading: false,
employeeInfo: {
- name: '寮犱笁',
- department: 'OPO椤圭洰閮�',
- position: '椤圭洰缁忕悊',
- employeeId: 'OPO001',
- phone: '138-1234-5678',
- avatar: ''
+ name: "",
+ department: "",
+ position: "",
+ employeeId: "",
+ phone: "",
+ avatar: ""
},
employeeStats: {
attendanceRate: 0,
@@ -105,137 +107,120 @@
},
attendanceData: [],
businessTripData: []
- }
+ };
},
created() {
- this.loadMockData()
-
- this.getEmployeeInfo()
- this.loadAttendanceData()
+ this.getEmployeeInfo();
+ this.loadAttendanceData();
},
methods: {
getEmployeeInfo() {
- const { employeeId, employeeName } = this.$route.query
+ const { employeeId, employeeName } = this.$route.query;
// 妯℃嫙鍛樺伐淇℃伅
this.employeeInfo = {
- name: employeeName || '寮犱笁',
- department: 'OPO椤圭洰閮�',
- position: '椤圭洰缁忕悊',
- employeeId: employeeId || 'OPO001',
- phone: '138****1234',
- avatar: ''
- }
- },
- loadMockData() {
- this.loading = true
-
- // 妯℃嫙寮傛鍔犺浇
- setTimeout(() => {
- const mockData = generateMockData()
- this.attendanceData = mockData.attendanceData
- this.businessTripData = mockData.businessTripData
- this.calculateStats()
- this.loading = false
- }, 500)
+ name: employeeName || "寮犱笁",
+ department: "OPO椤圭洰閮�",
+ position: "椤圭洰缁忕悊",
+ employeeId: employeeId || "OPO001",
+ phone: "138****1234",
+ avatar: ""
+ };
},
- calculateStats() {
- const totalDays = 31 // 12鏈堟湁31澶�
- const attendanceDays = this.attendanceData.filter(item =>
- item.status === 'present' || item.status === 'late'
- ).length
-
- const lateTimes = this.attendanceData.filter(item =>
- item.status === 'late'
- ).length
-
- // 璁$畻鍑哄樊鎬诲ぉ鏁�
- const businessTripDays = this.businessTripData.reduce((total, trip) => {
- const start = new Date(trip.startDate)
- const end = new Date(trip.endDate)
- const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
- return total + days
- }, 0)
-
- // 璁$畻鎬诲伐浣滄椂闀�
- const totalWorkHours = this.attendanceData.reduce((total, item) => {
- return total + (item.workHours || 0)
- }, 0)
-
- this.employeeStats = {
- attendanceRate: Math.round((attendanceDays / totalDays) * 100),
- workHours: totalWorkHours.toFixed(1),
- businessTripDays: businessTripDays,
- lateTimes: lateTimes,
- leaveEarlyTimes: this.attendanceData.filter(item =>
- item.status === 'leaveEarly'
- ).length
- }
- },
async loadAttendanceData() {
- this.loading = true
+ this.loading = true;
try {
- await new Promise(resolve => setTimeout(resolve, 500))
+ await new Promise(resolve => setTimeout(resolve, 500));
// 鐢熸垚涓汉鑰冨嫟妯℃嫙鏁版嵁
- this.attendanceData = this.generatePersonalAttendanceData()
- this.businessTripData = this.generatePersonalBusinessTripData()
- this.calculateStats()
+ this.attendanceData = this.generatePersonalAttendanceData();
+ this.businessTripData = this.generatePersonalBusinessTripData();
+ this.calculateStats();
} catch (error) {
- console.error('鍔犺浇鏁版嵁澶辫触:', error)
+ console.error("鍔犺浇鏁版嵁澶辫触:", error);
} finally {
- this.loading = false
+ this.loading = false;
}
},
generatePersonalAttendanceData() {
- const data = []
- const currentMonth = 12 // 12鏈�
+ const data = [];
+ const currentMonth = 12; // 12鏈�
for (let day = 1; day <= 31; day++) {
- if (Math.random() > 0.2) { // 80%鐨勫嚭鍕ょ巼
+ if (Math.random() > 0.2) {
+ // 80%鐨勫嚭鍕ょ巼
data.push({
id: day,
- date: `2024-${currentMonth.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`,
- checkIn: `08:${String(Math.floor(Math.random() * 30)).padStart(2, '0')}`,
- checkOut: `18:${String(Math.floor(Math.random() * 30)).padStart(2, '0')}`,
- status: Math.random() > 0.1 ? '姝e父' : '杩熷埌',
+ 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 ? "姝e父" : "杩熷埌",
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>
diff --git a/src/views/OfficeRelated/checkingIn/components/AttendanceCalendar.vue b/src/views/OfficeRelated/checkingIn/components/AttendanceCalendar.vue
index c43976e..5f90278 100644
--- a/src/views/OfficeRelated/checkingIn/components/AttendanceCalendar.vue
+++ b/src/views/OfficeRelated/checkingIn/components/AttendanceCalendar.vue
@@ -1,132 +1,23 @@
<template>
<div class="attendance-calendar">
- <!-- 鏃ュ巻澶撮儴缁熻淇℃伅 -->
- <div class="calendar-stats">
- <div class="stat-item">
- <span class="stat-dot present"></span>
- <span>姝e父鍑哄嫟: {{ stats.presentDays }}澶�</span>
- </div>
- <div class="stat-item">
- <span class="stat-dot absent"></span>
- <span>缂哄嫟/寮傚父: {{ stats.absentDays }}澶�</span>
- </div>
- <div class="stat-item">
- <span class="stat-dot trip"></span>
- <span>鍑哄樊: {{ stats.tripDays }}澶�</span>
- </div>
- <div class="stat-item">
- <span class="stat-dot late"></span>
- <span>杩熷埌/鏃╅��: {{ stats.lateDays }}澶�</span>
- </div>
- </div>
-
- <!-- Element UI 鏃ュ巻缁勪欢 -->
- <el-calendar v-model="calendarValue" :first-day-of-week="1">
+ <el-calendar v-model="calendarValue">
<template #date-cell="{ data }">
- <div
- class="calendar-date"
- :class="getDateStatusClass(data.day)"
- @click="handleDateClick(data)"
- >
+ <div class="calendar-date">
<div class="date-number">{{ data.day.split('-')[2] }}</div>
-
- <!-- 鐘舵�佽儗鏅壊鍧� -->
- <div class="status-background" :class="getBackgroundStatus(data.day)"></div>
-
<div class="date-events">
- <!-- 鍑哄嫟鐘舵�佹爣璁� -->
- <div v-if="getAttendanceStatus(data.day) !== 'absent'" class="status-mark">
- <el-tooltip
- :content="getAttendanceTooltip(data.day)"
- placement="top"
- >
- <span class="status-dot" :class="getAttendanceStatus(data.day)"></span>
- </el-tooltip>
- </div>
-
- <!-- 鍑哄樊鏍囪 -->
- <div v-if="hasBusinessTrip(data.day)" class="trip-mark">
- <el-tooltip content="鍑哄樊" placement="top">
- <i class="el-icon-location-outline"></i>
- </el-tooltip>
- </div>
-
- <!-- 缂哄嫟鏍囪 -->
- <div v-if="getAttendanceStatus(data.day) === 'absent'" class="absent-mark">
- <el-tooltip content="缂哄嫟" placement="top">
- <i class="el-icon-close"></i>
- </el-tooltip>
- </div>
- </div>
-
- <!-- 绠�鐣ヤ俊鎭樉绀� -->
- <div class="brief-info">
- <div v-if="getAttendanceStatus(data.day) === 'present'" class="info-item present-info">
- 鈭�
- </div>
- <div v-else-if="getAttendanceStatus(data.day) === 'late'" class="info-item late-info">
- !
- </div>
- <div v-else-if="getAttendanceStatus(data.day) === 'absent'" class="info-item absent-info">
- 脳
- </div>
- <div v-if="hasBusinessTrip(data.day)" class="info-item trip-info">
- 鉁�
- </div>
- </div>
-
- <!-- 鏃ユ湡璇︾粏淇℃伅锛堟偓娴樉绀猴級 -->
- <div class="date-details">
<div
v-for="event in getDateEvents(data.day)"
:key="event.id"
- class="detail-item"
+ class="event-item"
+ :class="event.type"
>
- <span class="detail-type">{{ event.type === 'attendance' ? '鍑哄嫟' : '鍑哄樊' }}</span>
- <span class="detail-info">{{ event.text }}</span>
- </div>
- <div v-if="getDateEvents(data.day).length === 0" class="detail-item">
- <span class="detail-type">鏃犺褰�</span>
+ <span class="event-dot"></span>
+ <span class="event-text">{{ event.text }}</span>
</div>
</div>
</div>
</template>
</el-calendar>
-
- <!-- 鏃ユ湡璇︽儏瀵硅瘽妗� -->
- <el-dialog
- :title="`${selectedDate} 鑰冨嫟璇︽儏`"
- v-model="detailDialogVisible"
- width="500px"
- >
- <div v-if="selectedDateInfo">
- <div class="detail-section">
- <h4>鍑哄嫟淇℃伅</h4>
- <div v-if="selectedDateInfo.attendance">
- <p>涓婄彮鏃堕棿: {{ selectedDateInfo.attendance.checkIn || '鏈墦鍗�' }}</p>
- <p>涓嬬彮鏃堕棿: {{ selectedDateInfo.attendance.checkOut || '鏈墦鍗�' }}</p>
- <p>鐘舵��:
- <el-tag :type="getStatusTagType(selectedDateInfo.attendance.status)">
- {{ getStatusText(selectedDateInfo.attendance.status) }}
- </el-tag>
- </p>
- </div>
- <div v-else>
- <p class="no-data">鏃犲嚭鍕よ褰�</p>
- </div>
- </div>
-
- <div class="detail-section" v-if="selectedDateInfo.businessTrip">
- <h4>鍑哄樊淇℃伅</h4>
- <p>鐩殑鍦�: {{ selectedDateInfo.businessTrip.destination }}</p>
- <p>浜嬬敱: {{ selectedDateInfo.businessTrip.reason }}</p>
- <p>閲岀▼: {{ selectedDateInfo.businessTrip.distance }}鍏噷</p>
- </div>
- </div>
- <template #footer>
- <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
- </template>
- </el-dialog>
</div>
</template>
@@ -134,211 +25,41 @@
export default {
name: 'AttendanceCalendar',
props: {
- attendanceData: {
- type: Array,
- default: () => []
- },
- businessTripData: {
- type: Array,
- default: () => []
- }
+ attendanceData: Array,
+ businessTripData: Array
},
data() {
return {
- calendarValue: new Date(),
- detailDialogVisible: false,
- selectedDate: '',
- selectedDateInfo: null,
- stats: {
- presentDays: 0,
- absentDays: 0,
- tripDays: 0,
- lateDays: 0
- }
- }
- },
- mounted() {
- this.calculateStats()
- },
- watch: {
- attendanceData: {
- handler() {
- this.calculateStats()
- },
- deep: true
- },
- businessTripData: {
- handler() {
- this.calculateStats()
- },
- deep: true
+ calendarValue: new Date()
}
},
methods: {
- // 鑾峰彇鏃ユ湡鐘舵�佺被鍚�
- getDateStatusClass(date) {
- const classes = []
- if (this.isToday(date)) {
- classes.push('today')
- }
- if (this.isSelected(date)) {
- classes.push('selected')
- }
-
- // 娣诲姞鐘舵�佺被鍚�
- const status = this.getBackgroundStatus(date)
- if (status) {
- classes.push(status)
- }
-
- return classes
- },
-
- // 鑾峰彇鑳屾櫙鐘舵�侊紙鐢ㄤ簬鑳屾櫙鑹诧級
- getBackgroundStatus(date) {
- const attendance = this.attendanceData.find(item => item.date === date)
- const hasTrip = this.hasBusinessTrip(date)
-
- if (hasTrip) {
- return 'has-trip'
- }
-
- if (attendance) {
- switch (attendance.status) {
- case 'present': return 'has-attendance'
- case 'late': return 'has-late'
- case 'absent': return 'has-absent'
- default: return ''
- }
- }
-
- return ''
- },
-
- // 鍒ゆ柇鏄惁涓轰粖澶�
- isToday(date) {
- const today = new Date()
- const compareDate = new Date(date)
- return today.toDateString() === compareDate.toDateString()
- },
-
- // 鍒ゆ柇鏄惁琚�変腑
- isSelected(date) {
- return this.selectedDate === date
- },
-
- // 鑾峰彇鑰冨嫟鐘舵��
- getAttendanceStatus(date) {
- const attendance = this.attendanceData.find(item => item.date === date)
- if (!attendance) return 'absent'
-
- switch (attendance.status) {
- case 'present': return 'present'
- case 'late': return 'late'
- case 'absent': return 'absent'
- default: return 'absent'
- }
- },
-
- // 鑾峰彇鐘舵�佹枃鏈�
- getStatusText(status) {
- const statusMap = {
- present: '姝e父鍑哄嫟',
- late: '杩熷埌/鏃╅��',
- absent: '缂哄嫟/寮傚父'
- }
- return statusMap[status] || '鏈煡鐘舵��'
- },
-
- // 鑾峰彇鑰冨嫟鐘舵�佹彁绀�
- getAttendanceTooltip(date) {
- const statusMap = {
- present: '姝e父鍑哄嫟',
- late: '杩熷埌/鏃╅��',
- absent: '缂哄嫟/寮傚父'
- }
- return statusMap[this.getAttendanceStatus(date)] || '鏃犺褰�'
- },
-
- // 鍒ゆ柇鏄惁鏈夊嚭宸�
- hasBusinessTrip(date) {
- return this.businessTripData.some(item =>
- date >= item.startDate && date <= item.endDate
- )
- },
-
- // 鑾峰彇鏃ユ湡浜嬩欢
getDateEvents(date) {
const events = []
- const attendance = this.attendanceData.find(item => item.date === date)
+ // 妫�鏌ュ嚭鍕よ褰�
+ const attendance = this.attendanceData.find(item => item.date === date)
if (attendance) {
events.push({
id: `attendance-${date}`,
type: 'attendance',
- text: `${attendance.checkIn || '鏈墦鍗�'} - ${attendance.checkOut || '鏈墦鍗�'}`
+ text: `${attendance.checkIn}-${attendance.checkOut}`
})
}
+ // 妫�鏌ュ嚭宸褰�
const businessTrip = this.businessTripData.find(item =>
date >= item.startDate && date <= item.endDate
)
if (businessTrip) {
events.push({
id: `business-trip-${date}`,
- type: 'businessTrip',
- text: `鍓嶅線${businessTrip.destination}`
+ type: 'business-trip',
+ text: `鍑哄樊: ${businessTrip.endCity}`
})
}
return events
- },
-
- // 澶勭悊鏃ユ湡鐐瑰嚮浜嬩欢
- handleDateClick(data) {
- this.selectedDate = data.day
- this.selectedDateInfo = {
- attendance: this.attendanceData.find(item => item.date === data.day),
- businessTrip: this.businessTripData.find(item =>
- data.day >= item.startDate && data.day <= item.endDate
- )
- }
- this.detailDialogVisible = true
- },
-
- // 鑾峰彇鐘舵�佹爣绛剧被鍨�
- getStatusTagType(status) {
- const typeMap = {
- present: 'success',
- late: 'warning',
- absent: 'danger'
- }
- return typeMap[status] || 'info'
- },
-
- // 璁$畻缁熻淇℃伅
- calculateStats() {
- // 閲嶇疆缁熻
- this.stats = { presentDays: 0, absentDays: 0, tripDays: 0, lateDays: 0 }
-
- this.attendanceData.forEach(item => {
- switch (item.status) {
- case 'present': this.stats.presentDays++; break
- case 'late': this.stats.lateDays++; break
- case 'absent': this.stats.absentDays++; break
- }
- })
-
- // 璁$畻鍑哄樊澶╂暟
- const tripDays = new Set()
- this.businessTripData.forEach(item => {
- const start = new Date(item.startDate)
- const end = new Date(item.endDate)
- for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
- tripDays.add(d.toISOString().split('T')[0])
- }
- })
- this.stats.tripDays = tripDays.size
}
}
}
@@ -347,242 +68,49 @@
<style scoped>
.attendance-calendar {
padding: 20px;
- max-width: 100%;
}
-
-.calendar-stats {
- display: flex;
- justify-content: space-around;
- margin-bottom: 20px;
- padding: 15px;
- background: #f5f7fa;
- border-radius: 8px;
-}
-
-.stat-item {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.stat-dot {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- display: inline-block;
-}
-
-.stat-dot.present { background-color: #67c23a; }
-.stat-dot.absent { background-color: #f56c6c; }
-.stat-dot.trip { background-color: #409eff; }
-.stat-dot.late { background-color: #e6a23c; }
.calendar-date {
- height: 80px;
- padding: 4px;
- border: 1px solid #ebeef5;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.3s;
- position: relative;
- overflow: hidden;
-}
-
-.calendar-date:hover {
- background-color: #f0f9ff;
- border-color: #409eff;
- transform: translateY(-2px);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-
-.calendar-date.today {
- border-color: #409eff;
- background-color: #f0f9ff;
-}
-
-.calendar-date.selected {
- background-color: #ecf5ff;
- border-color: #409eff;
-}
-
-/* 鐘舵�佽儗鏅壊 */
-.status-background {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- opacity: 0.1;
- z-index: 0;
-}
-
-.calendar-date.has-attendance .status-background {
- background-color: #67c23a;
-}
-
-.calendar-date.has-late .status-background {
- background-color: #e6a23c;
-}
-
-.calendar-date.has-absent .status-background {
- background-color: #f56c6c;
-}
-
-.calendar-date.has-trip .status-background {
- background: linear-gradient(135deg, #409eff 0%, #67c23a 100%);
+ height: 100%;
+ display: flex;
+ flex-direction: column;
}
.date-number {
font-weight: bold;
- font-size: 14px;
- margin-bottom: 2px;
- position: relative;
- z-index: 1;
+ margin-bottom: 4px;
}
.date-events {
- display: flex;
- flex-direction: column;
- gap: 2px;
- position: relative;
- z-index: 1;
+ flex: 1;
+ overflow: hidden;
}
-.status-mark, .trip-mark, .absent-mark {
+.event-item {
display: flex;
align-items: center;
- gap: 4px;
+ margin-bottom: 2px;
+ font-size: 12px;
}
-.status-dot {
- width: 8px;
- height: 8px;
+.event-dot {
+ width: 6px;
+ height: 6px;
border-radius: 50%;
- display: inline-block;
-}
-
-.status-dot.present { background-color: #67c23a; }
-.status-dot.late { background-color: #e6a23c; }
-.status-dot.absent { background-color: #f56c6c; }
-
-.trip-mark i {
- color: #409eff;
- font-size: 12px;
-}
-
-.absent-mark i {
- color: #f56c6c;
- font-size: 12px;
-}
-
-/* 绠�鐣ヤ俊鎭樉绀� */
-.brief-info {
- position: absolute;
- bottom: 4px;
- right: 4px;
- display: flex;
- gap: 2px;
- z-index: 1;
-}
-
-.info-item {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 10px;
- font-weight: bold;
-}
-
-.present-info {
- background-color: #67c23a;
- color: white;
-}
-
-.late-info {
- background-color: #e6a23c;
- color: white;
-}
-
-.absent-info {
- background-color: #f56c6c;
- color: white;
-}
-
-.trip-info {
- background-color: #409eff;
- color: white;
-}
-
-.date-details {
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 8px;
- z-index: 1000;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
- display: none;
-}
-
-.calendar-date:hover .date-details {
- display: block;
-}
-
-.detail-item {
- font-size: 12px;
- margin-bottom: 4px;
- display: flex;
- align-items: center;
-}
-
-.detail-type {
- font-weight: bold;
margin-right: 4px;
- min-width: 40px;
}
-.detail-info {
- color: #606266;
+.event-item.attendance .event-dot {
+ background-color: #67c23a;
}
-.detail-section {
- margin-bottom: 20px;
+.event-item.business-trip .event-dot {
+ background-color: #409eff;
}
-.detail-section h4 {
- margin-bottom: 10px;
- color: #303133;
- border-left: 4px solid #409eff;
- padding-left: 8px;
-}
-
-.no-data {
- color: #909399;
- font-style: italic;
-}
-
-:deep(.el-calendar__header) {
- padding: 10px;
- border-bottom: 1px solid #ebeef5;
-}
-
-:deep(.el-calendar-day) {
- padding: 0 !important;
- height: 80px;
-}
-
-:deep(.el-calendar-table:not(.is-range) td) {
- border: 1px solid #f0f0f0;
-}
-
-:deep(.el-calendar-table .el-calendar-day) {
- height: 80px !important;
- padding: 0 !important;
+.event-text {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
</style>
diff --git a/src/views/OfficeRelated/checkingIn/components/PersonalAttendanceReport.vue b/src/views/OfficeRelated/checkingIn/components/PersonalAttendanceReport.vue
index 8c5d0bd..0771f71 100644
--- a/src/views/OfficeRelated/checkingIn/components/PersonalAttendanceReport.vue
+++ b/src/views/OfficeRelated/checkingIn/components/PersonalAttendanceReport.vue
@@ -12,6 +12,7 @@
size="small"
@click="exportReport"
icon="el-icon-download"
+ type="primary"
>
瀵煎嚭鎶ヨ〃
</el-button>
@@ -20,7 +21,7 @@
<!-- 缁熻姒傝 -->
<el-row :gutter="20" class="stats-overview">
- <el-col :span="6">
+ <el-col :xs="12" :sm="6" class="stat-col">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon attendance-icon">
@@ -33,7 +34,7 @@
</div>
</el-card>
</el-col>
- <el-col :span="6">
+ <el-col :xs="12" :sm="6" class="stat-col">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon present-icon">
@@ -46,7 +47,7 @@
</div>
</el-card>
</el-col>
- <el-col :span="6">
+ <el-col :xs="12" :sm="6" class="stat-col">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon abnormal-icon">
@@ -59,7 +60,7 @@
</div>
</el-card>
</el-col>
- <el-col :span="6">
+ <el-col :xs="12" :sm="6" class="stat-col">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon rate-icon">
@@ -74,98 +75,150 @@
</el-col>
</el-row>
- <!-- 鍥捐〃鍖哄煙 -->
+ <!-- 鍥捐〃鍖哄煙 - 浼樺寲甯冨眬 -->
<el-row :gutter="20" class="charts-section">
- <el-col :span="12">
- <el-card header="鍑哄嫟瓒嬪娍" shadow="never">
+ <el-col :xs="24" :lg="12" class="chart-col">
+ <el-card class="chart-card" shadow="never">
+ <template #header>
+ <div class="chart-header">
+ <span class="chart-title">鍑哄嫟瓒嬪娍鍒嗘瀽</span>
+ <div class="chart-legend">
+ <span class="legend-item">
+ <span class="legend-color bar-color"></span>
+ 鍑哄嫟澶╂暟
+ </span>
+ <span class="legend-item">
+ <span class="legend-color line-color"></span>
+ 鍑哄嫟鐜�
+ </span>
+ </div>
+ </div>
+ </template>
<div id="attendanceTrendChart" class="chart-container"></div>
</el-card>
</el-col>
- <el-col :span="12">
- <el-card header="鑰冨嫟鍒嗗竷" shadow="never">
+ <el-col :xs="24" :lg="12" class="chart-col">
+ <el-card class="chart-card" shadow="never">
+ <template #header>
+ <div class="chart-header">
+ <span class="chart-title">鑰冨嫟鍒嗗竷</span>
+ </div>
+ </template>
<div id="attendanceDistributionChart" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 璇︾粏缁熻琛ㄦ牸 -->
- <el-card header="璇︾粏缁熻" class="detail-table-card" shadow="never">
- <el-table :data="detailedStats" border style="width: 100%">
- <el-table-column prop="month" label="鏈堜唤" />
- <el-table-column prop="workDays" label="搴斿嚭鍕ゅぉ鏁�" />
- <el-table-column prop="actualDays" label="瀹為檯鍑哄嫟" />
- <el-table-column prop="lateTimes" label="杩熷埌娆℃暟" >
- <template #default="scope">
- <el-tag v-if="scope.row.lateTimes > 0" type="warning" size="small">
- {{ scope.row.lateTimes }}
- </el-tag>
- <span v-else>{{ scope.row.lateTimes }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="leaveEarlyTimes" label="鏃╅��娆℃暟" >
- <template #default="scope">
- <el-tag v-if="scope.row.leaveEarlyTimes > 0" type="warning" size="small">
- {{ scope.row.leaveEarlyTimes }}
- </el-tag>
- <span v-else>{{ scope.row.leaveEarlyTimes }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="absenceDays" label="缂哄嫟澶╂暟" >
- <template #default="scope">
- <el-tag v-if="scope.row.absenceDays > 0" type="danger" size="small">
- {{ scope.row.absenceDays }}
- </el-tag>
- <span v-else>{{ scope.row.absenceDays }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="businessTripDays" label="鍑哄樊澶╂暟" >
- <template #default="scope">
- <el-tag v-if="scope.row.businessTripDays > 0" type="primary" size="small">
- {{ scope.row.businessTripDays }}
- </el-tag>
- <span v-else>{{ scope.row.businessTripDays }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="attendanceRate" label="鍑哄嫟鐜�" >
- <template #default="scope">
- <el-progress
- :percentage="scope.row.attendanceRate"
- :show-text="false"
- :color="getProgressColor(scope.row.attendanceRate)"
- />
- <span>{{ scope.row.attendanceRate }}%</span>
- </template>
- </el-table-column>
- </el-table>
+ <el-card class="detail-card" shadow="never">
+ <template #header>
+ <div class="card-header">
+ <span class="card-title">鏈堝害璇︾粏缁熻</span>
+ <el-button size="mini" type="text" @click="toggleTableExpand">
+ {{ tableExpanded ? '鏀惰捣' : '灞曞紑' }}
+ <i :class="tableExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
+ </el-button>
+ </div>
+ </template>
+ <el-collapse-transition>
+ <div v-show="tableExpanded">
+ <el-table
+ :data="detailedStats"
+ border
+ style="width: 100%"
+ class="stats-table"
+ :row-class-name="getRowClassName"
+ >
+ <!-- 琛ㄦ牸鍒楀畾涔変繚鎸佷笉鍙� -->
+ <el-table-column prop="month" label="鏈堜唤" align="center" />
+ <el-table-column prop="workDays" label="搴斿嚭鍕ゅぉ鏁�" align="center" />
+ <el-table-column prop="actualDays" label="瀹為檯鍑哄嫟" align="center" />
+ <el-table-column prop="lateTimes" label="杩熷埌娆℃暟" align="center">
+ <template #default="scope">
+ <el-tag v-if="scope.row.lateTimes > 0" type="warning" size="small">
+ {{ scope.row.lateTimes }}
+ </el-tag>
+ <span v-else>{{ scope.row.lateTimes }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="leaveEarlyTimes" label="鏃╅��娆℃暟" align="center">
+ <template #default="scope">
+ <el-tag v-if="scope.row.leaveEarlyTimes > 0" type="warning" size="small">
+ {{ scope.row.leaveEarlyTimes }}
+ </el-tag>
+ <span v-else>{{ scope.row.leaveEarlyTimes }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="absenceDays" label="缂哄嫟澶╂暟" align="center">
+ <template #default="scope">
+ <el-tag v-if="scope.row.absenceDays > 0" type="danger" size="small">
+ {{ scope.row.absenceDays }}
+ </el-tag>
+ <span v-else>{{ scope.row.absenceDays }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="businessTripDays" label="鍑哄樊澶╂暟" align="center">
+ <template #default="scope">
+ <el-tag v-if="scope.row.businessTripDays > 0" type="primary" size="small">
+ {{ scope.row.businessTripDays }}
+ </el-tag>
+ <span v-else>{{ scope.row.businessTripDays }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="attendanceRate" label="鍑哄嫟鐜�" align="center" min-width="100">
+ <template #default="scope">
+ <div class="progress-cell">
+ <el-progress
+ :percentage="scope.row.attendanceRate"
+ :show-text="false"
+ :color="getProgressColor(scope.row.attendanceRate)"
+ class="rate-progress"
+ />
+ <span class="rate-text" :class="getRateTextClass(scope.row.attendanceRate)">
+ {{ scope.row.attendanceRate }}%
+ </span>
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-collapse-transition>
</el-card>
<!-- 寮傚父璁板綍 -->
- <el-card header="寮傚父璁板綍" class="abnormal-records-card" shadow="never">
- <el-table :data="abnormalRecords" border style="width: 100%">
- <el-table-column prop="date" label="鏃ユ湡" />
- <el-table-column prop="type" label="寮傚父绫诲瀷" >
+ <el-card class="abnormal-card" shadow="never">
+ <template #header>
+ <div class="card-header">
+ <span class="card-title">寮傚父璁板綍鏄庣粏</span>
+ </div>
+ </template>
+ <el-table :data="abnormalRecords" border class="abnormal-table">
+ <!-- 寮傚父璁板綍琛ㄦ牸鍒楀畾涔変繚鎸佷笉鍙� -->
+ <el-table-column prop="date" label="鏃ユ湡" align="center" />
+ <el-table-column prop="type" label="寮傚父绫诲瀷" align="center">
<template #default="scope">
- <el-tag :type="getAbnormalType(scope.row.type)" size="small">
+ <el-tag :type="getAbnormalType(scope.row.type)" size="small" effect="light">
{{ scope.row.type }}
</el-tag>
</template>
</el-table-column>
- <el-table-column prop="description" label="鎻忚堪" min-width="200" />
- <el-table-column prop="duration" label="鏃堕暱" />
- <el-table-column prop="status" label="鐘舵��" >
+ <el-table-column prop="description" label="鎻忚堪" align="center" min-width="200" />
+ <el-table-column prop="duration" label="鏃堕暱" align="center" />
+ <el-table-column prop="status" label="鐘舵��" align="center">
<template #default="scope">
<el-tag
:type="scope.row.status === '宸插鐞�' ? 'success' : 'warning'"
size="small"
+ effect="light"
>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
- <el-table-column label="鎿嶄綔" >
+ <el-table-column label="鎿嶄綔" align="center" width="80">
<template #default="scope">
<el-button type="text" size="mini" @click="viewAbnormalDetail(scope.row)">
- 鏌ョ湅
+ 璇︽儏
</el-button>
</template>
</el-table-column>
@@ -201,7 +254,8 @@
detailedStats: [],
abnormalRecords: [],
trendChart: null,
- distributionChart: null
+ distributionChart: null,
+ tableExpanded: true
}
},
mounted() {
@@ -211,7 +265,7 @@
})
},
beforeDestroy() {
- // 閿�姣佸浘琛ㄥ疄渚�
+ // 閿�姣佸浘琛ㄥ疄渚媅3](@ref)
if (this.trendChart) {
this.trendChart.dispose()
}
@@ -221,7 +275,7 @@
},
methods: {
initData() {
- // 鍒濆鍖栨瑙堟暟鎹�
+ // 鍒濆鍖栨暟鎹�昏緫淇濇寔涓嶅彉
this.overview = {
totalDays: this.stats.totalDays || 22,
presentDays: this.stats.presentDays || 20,
@@ -229,7 +283,6 @@
attendanceRate: this.stats.attendanceRate || 90.9
}
- // 鍒濆鍖栬缁嗙粺璁�
this.detailedStats = [
{ month: '2024-12', workDays: 22, actualDays: 20, lateTimes: 2,
leaveEarlyTimes: 1, absenceDays: 0, businessTripDays: 3, attendanceRate: 90.9 },
@@ -239,7 +292,6 @@
leaveEarlyTimes: 0, absenceDays: 0, businessTripDays: 1, attendanceRate: 95.7 }
]
- // 鍒濆鍖栧紓甯歌褰�
this.abnormalRecords = [
{ date: '2024-12-15', type: '杩熷埌', description: '鏃╀笂杩熷埌30鍒嗛挓', duration: '30鍒嗛挓', status: '宸插鐞�' },
{ date: '2024-12-08', type: '鏃╅��', description: '涓嬪崍鎻愬墠1灏忔椂绂诲紑', duration: '1灏忔椂', status: '宸插鐞�' },
@@ -261,43 +313,89 @@
const option = {
tooltip: {
trigger: 'axis',
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
+ borderColor: '#ebeef5',
+ borderWidth: 1,
+ textStyle: {
+ color: '#606266'
+ },
formatter: function(params) {
- let result = params[0].axisValue + '<br/>'
+ let result = `<div style="font-weight: 600; margin-bottom: 8px;">${params[0].axisValue}</div>`
params.forEach(param => {
- result += `${param.seriesName}: ${param.value}<br/>`
+ const icon = param.seriesType === 'bar' ? '鈼�' : '鈼�'
+ result += `<div>${icon} ${param.seriesName}: <span style="font-weight: 600; color: ${param.color}">${param.value}${param.seriesName === '鍑哄嫟鐜�' ? '%' : ''}</span></div>`
})
return result
}
},
legend: {
data: ['鍑哄嫟澶╂暟', '寮傚父澶╂暟', '鍑哄嫟鐜�'],
- bottom: 10
+ bottom: 10,
+ textStyle: {
+ color: '#606266'
+ },
+ itemWidth: 12,
+ itemHeight: 12
},
grid: {
left: '3%',
- right: '4%',
+ right: '3%',
bottom: '15%',
- top: '10%',
+ top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
- data: ['10鏈�', '11鏈�', '12鏈�']
+ data: ['10鏈�', '11鏈�', '12鏈�'],
+ axisLine: {
+ lineStyle: {
+ color: '#dcdfe6'
+ }
+ },
+ axisLabel: {
+ color: '#606266'
+ }
},
yAxis: [
{
type: 'value',
name: '澶╂暟',
min: 0,
- max: 30
+ max: 30,
+ axisLine: {
+ show: true,
+ lineStyle: {
+ color: '#dcdfe6'
+ }
+ },
+ axisLabel: {
+ color: '#606266',
+ formatter: '{value}'
+ },
+ splitLine: {
+ lineStyle: {
+ color: '#f0f2f5',
+ type: 'dashed'
+ }
+ }
},
{
type: 'value',
name: '鍑哄嫟鐜�(%)',
min: 0,
max: 100,
+ axisLine: {
+ show: true,
+ lineStyle: {
+ color: '#dcdfe6'
+ }
+ },
axisLabel: {
+ color: '#606266',
formatter: '{value}%'
+ },
+ splitLine: {
+ show: false
}
}
],
@@ -305,19 +403,39 @@
{
name: '鍑哄嫟澶╂暟',
type: 'bar',
- barWidth: '30%',
+ barWidth: '25%',
data: [22, 19, 20],
itemStyle: {
- color: '#409EFF'
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: '#409EFF' },
+ { offset: 1, color: '#66b1ff' }
+ ]),
+ borderRadius: [2, 2, 0, 0]
+ },
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowColor: 'rgba(64, 158, 255, 0.5)'
+ }
}
},
{
name: '寮傚父澶╂暟',
type: 'bar',
- barWidth: '30%',
+ barWidth: '25%',
data: [1, 2, 2],
itemStyle: {
- color: '#F56C6C'
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: '#F56C6C' },
+ { offset: 1, color: '#f78989' }
+ ]),
+ borderRadius: [2, 2, 0, 0]
+ },
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowColor: 'rgba(245, 108, 108, 0.5)'
+ }
}
},
{
@@ -325,14 +443,28 @@
type: 'line',
yAxisIndex: 1,
data: [95.7, 90.5, 90.9],
+ symbol: 'circle',
+ symbolSize: 8,
itemStyle: {
- color: '#67C23A'
+ color: '#67C23A',
+ borderColor: '#fff',
+ borderWidth: 2
},
lineStyle: {
- width: 3
- }
+ width: 3,
+ shadowColor: 'rgba(103, 194, 58, 0.3)',
+ shadowBlur: 8,
+ shadowOffsetY: 2
+ },
+ emphasis: {
+ scale: true
+ },
+ animationEasing: 'cubicInOut',
+ animationDuration: 2000
}
- ]
+ ],
+ animation: true,
+ animationDuration: 1500
}
this.trendChart.setOption(option)
},
@@ -345,13 +477,27 @@
const option = {
tooltip: {
trigger: 'item',
- formatter: '{a} <br/>{b}: {c} ({d}%)'
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
+ borderColor: '#ebeef5',
+ borderWidth: 1,
+ textStyle: {
+ color: '#606266'
+ },
+ formatter: '{a} <br/>{b}: {c}澶� ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
- data: ['姝e父鍑哄嫟', '杩熷埌', '鏃╅��', '缂哄嫟', '鍑哄樊']
+ textStyle: {
+ color: '#606266',
+ fontSize: 12
+ },
+ itemWidth: 12,
+ itemHeight: 12,
+ formatter: function(name) {
+ return `${name}`
+ }
},
series: [
{
@@ -359,24 +505,38 @@
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'],
- avoidLabelOverlap: false,
+ avoidLabelOverlap: true,
itemStyle: {
borderColor: '#fff',
- borderWidth: 2
+ borderWidth: 2,
+ borderRadius: 3,
+ shadowBlur: 5,
+ shadowColor: 'rgba(0, 0, 0, 0.1)'
},
label: {
- show: false,
- position: 'center'
+ show: true,
+ formatter: '{b}\n{d}%',
+ textStyle: {
+ fontSize: 12,
+ fontWeight: 'normal'
+ }
},
emphasis: {
label: {
show: true,
- fontSize: 18,
+ fontSize: 14,
fontWeight: 'bold'
+ },
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.2)'
}
},
labelLine: {
- show: false
+ length: 15,
+ length2: 10,
+ smooth: true
},
data: [
{ value: 20, name: '姝e父鍑哄嫟', itemStyle: { color: '#67C23A' } },
@@ -384,23 +544,30 @@
{ value: 1, name: '鏃╅��', itemStyle: { color: '#F56C6C' } },
{ value: 0, name: '缂哄嫟', itemStyle: { color: '#909399' } },
{ value: 3, name: '鍑哄樊', itemStyle: { color: '#409EFF' } }
- ]
+ ],
+ animationType: 'scale',
+ animationEasing: 'elasticOut',
+ animationDelay: function(idx) {
+ return Math.random() * 200
+ }
}
- ]
+ ],
+ animation: true,
+ animationDuration: 1000
}
this.distributionChart.setOption(option)
},
setupChartResize() {
- // 鐩戝惉绐楀彛鍙樺寲锛岄噸鏂版覆鏌撳浘琛�
- const handleResize = () => {
+ // 浣跨敤鑺傛祦鍑芥暟浼樺寲鎬ц兘[3](@ref)
+ const handleResize = this.throttle(() => {
if (this.trendChart) {
this.trendChart.resize()
}
if (this.distributionChart) {
this.distributionChart.resize()
}
- }
+ }, 300)
window.addEventListener('resize', handleResize)
this.$once('hook:beforeDestroy', () => {
@@ -408,13 +575,42 @@
})
},
+ throttle(func, wait) {
+ let timeout = null
+ return function() {
+ const context = this
+ const args = arguments
+ if (!timeout) {
+ timeout = setTimeout(() => {
+ timeout = null
+ func.apply(context, args)
+ }, wait)
+ }
+ }
+ },
+
+ toggleTableExpand() {
+ this.tableExpanded = !this.tableExpanded
+ },
+
+ getRowClassName({ rowIndex }) {
+ return rowIndex % 2 === 1 ? 'even-row' : 'odd-row'
+ },
+
+ getRateTextClass(rate) {
+ if (rate >= 95) return 'rate-excellent'
+ if (rate >= 90) return 'rate-good'
+ if (rate >= 80) return 'rate-average'
+ return 'rate-poor'
+ },
+
+ // 鍏朵粬鏂规硶淇濇寔涓嶅彉
handlePeriodChange(period) {
this.reportPeriod = period
this.updateChartData()
},
updateChartData() {
- // 鏍规嵁閫夋嫨鐨勫懆鏈熸洿鏂板浘琛ㄦ暟鎹�
let data
switch (this.reportPeriod) {
case 'month':
@@ -429,12 +625,10 @@
default:
data = this.getMonthlyData()
}
-
this.updateCharts(data)
},
getMonthlyData() {
- // 妯℃嫙鏈堝害鏁版嵁
return {
xAxis: ['10鏈�', '11鏈�', '12鏈�'],
attendance: [22, 19, 20],
@@ -444,7 +638,6 @@
},
getQuarterlyData() {
- // 妯℃嫙瀛e害鏁版嵁
return {
xAxis: ['Q1', 'Q2', 'Q3', 'Q4'],
attendance: [65, 62, 58, 61],
@@ -454,7 +647,6 @@
},
getYearlyData() {
- // 妯℃嫙骞村害鏁版嵁
return {
xAxis: ['2022', '2023', '2024'],
attendance: [240, 248, 252],
@@ -470,7 +662,7 @@
option.series[0].data = data.attendance
option.series[1].data = data.abnormal
option.series[2].data = data.rate
- this.trendChart.setOption(option)
+ this.trendChart.setOption(option, { notMerge: false })
}
},
@@ -493,12 +685,10 @@
viewAbnormalDetail(record) {
this.$message.info(`鏌ョ湅寮傚父璁板綍: ${record.date} - ${record.type}`)
- // 杩欓噷鍙互鎵撳紑璇︽儏瀵硅瘽妗�
},
exportReport() {
this.$message.success('鎶ヨ〃瀵煎嚭鍔熻兘寮�鍙戜腑')
- // 杩欓噷鍙互瀹炵幇瀵煎嚭PDF鎴朎xcel鍔熻兘
}
}
}
@@ -506,9 +696,9 @@
<style scoped>
.personal-attendance-report {
- padding: 20px;
- background: #fff;
- border-radius: 8px;
+ padding: 24px;
+ background: #f8f9fa;
+ min-height: 100vh;
}
.report-header {
@@ -516,15 +706,17 @@
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
- padding-bottom: 16px;
- border-bottom: 1px solid #ebeef5;
+ padding: 0;
}
.report-header h4 {
margin: 0;
color: #303133;
- font-size: 20px;
+ font-size: 24px;
font-weight: 600;
+ background: linear-gradient(135deg, #409EFF, #67C23A);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
}
.header-actions {
@@ -534,51 +726,78 @@
}
.stats-overview {
- margin-bottom: 24px;
+ margin-bottom: 32px;
+}
+
+.stat-col {
+ margin-bottom: 20px;
}
.stat-card {
- border-radius: 8px;
- transition: all 0.3s ease;
+ border-radius: 12px;
+ border: none;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ background: #fff;
+ position: relative;
+ overflow: hidden;
+}
+
+.stat-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: linear-gradient(90deg, var(--gradient-start), var(--gradient-end));
}
.stat-card:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transform: translateY(-4px);
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
- padding: 16px;
+ padding: 20px;
}
.stat-icon {
- width: 60px;
- height: 60px;
- border-radius: 50%;
+ width: 64px;
+ height: 64px;
+ border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
- font-size: 24px;
+ font-size: 28px;
color: white;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.attendance-icon {
- background: linear-gradient(135deg, #409EFF, #79BBFF);
+ --gradient-start: #409EFF;
+ --gradient-end: #66b1ff;
+ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
}
.present-icon {
- background: linear-gradient(135deg, #67C23A, #95D475);
+ --gradient-start: #67C23A;
+ --gradient-end: #85ce61;
+ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
}
.abnormal-icon {
- background: linear-gradient(135deg, #E6A23C, #EEBD6D);
+ --gradient-start: #E6A23C;
+ --gradient-end: #ebb563;
+ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
}
.rate-icon {
- background: linear-gradient(135deg, #F56C6C, #F89898);
+ --gradient-start: #F56C6C;
+ --gradient-end: #f78989;
+ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
}
.stat-info {
@@ -586,47 +805,162 @@
}
.stat-value {
- font-size: 28px;
- font-weight: bold;
+ font-size: 32px;
+ font-weight: 700;
color: #303133;
margin-bottom: 4px;
+ line-height: 1;
}
.stat-label {
color: #909399;
font-size: 14px;
+ font-weight: 500;
}
.charts-section {
+ margin-bottom: 32px;
+}
+
+.chart-col {
margin-bottom: 24px;
}
-.chart-container {
- width: 100%;
- height: 300px;
+.chart-card {
+ border-radius: 12px;
+ border: none;
+ background: #fff;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
-.detail-table-card,
-.abnormal-records-card {
- margin-bottom: 20px;
+.chart-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0;
+}
+
+.chart-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.chart-legend {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: #606266;
+}
+
+.legend-color {
+ width: 12px;
+ height: 12px;
+ border-radius: 2px;
+}
+
+.bar-color {
+ background: linear-gradient(135deg, #409EFF, #66b1ff);
+}
+
+.line-color {
+ background: linear-gradient(135deg, #67C23A, #85ce61);
+}
+
+.chart-container {
+ width: 40vw;
+ height: 320px;
+ position: relative;
+}
+
+.detail-card,
+.abnormal-card {
+ border-radius: 12px;
+ border: none;
+ background: #fff;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ margin-bottom: 24px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0;
+}
+
+.card-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.stats-table {
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.stats-table :deep(.el-table__row) {
+ transition: background-color 0.3s;
+}
+
+.stats-table :deep(.el-table__row:hover) {
+ background-color: #f5f7fa;
+}
+
+.stats-table :deep(.even-row) {
+ background-color: #fafbfc;
+}
+
+.progress-cell {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.rate-progress {
+ flex: 1;
+}
+
+.rate-text {
+ font-size: 12px;
+ font-weight: 600;
+ min-width: 40px;
+}
+
+.rate-excellent { color: #67C23A; }
+.rate-good { color: #E6A23C; }
+.rate-average { color: #F56C6C; }
+.rate-poor { color: #909399; }
+
+.abnormal-table {
+ border-radius: 8px;
+ overflow: hidden;
}
/* 鍝嶅簲寮忚璁� */
@media (max-width: 1200px) {
- .stats-overview .el-col {
- margin-bottom: 16px;
+ .chart-container {
+ height: 280px;
}
}
@media (max-width: 768px) {
.personal-attendance-report {
- padding: 12px;
+ padding: 16px;
}
.report-header {
flex-direction: column;
align-items: flex-start;
- gap: 12px;
+ gap: 16px;
}
.header-actions {
@@ -635,14 +969,42 @@
}
.stat-content {
- padding: 12px;
+ padding: 16px;
+ flex-direction: column;
+ text-align: center;
}
.stat-icon {
- width: 50px;
- height: 50px;
- font-size: 20px;
- margin-right: 12px;
+ margin-right: 0;
+ margin-bottom: 12px;
+ width: 56px;
+ height: 56px;
+ font-size: 24px;
+ }
+
+ .stat-value {
+ font-size: 28px;
+ }
+
+ .chart-container {
+ height: 240px;
+ }
+
+ .chart-header {
+ flex-direction: column;
+ gap: 12px;
+ align-items: flex-start;
+ }
+
+ .chart-legend {
+ align-self: stretch;
+ justify-content: space-around;
+ }
+}
+
+@media (max-width: 480px) {
+ .stat-col {
+ margin-bottom: 16px;
}
.stat-value {
@@ -650,7 +1012,7 @@
}
.chart-container {
- height: 250px;
+ height: 200px;
}
}
@@ -662,21 +1024,23 @@
opacity: 0;
}
-/* 琛ㄦ牸鏍峰紡浼樺寲 */
-.el-table {
- border-radius: 4px;
- overflow: hidden;
+/* 婊氬姩鏉℃牱寮忎紭鍖� */
+:deep(::-webkit-scrollbar) {
+ width: 6px;
+ height: 6px;
}
-.el-table::before {
- display: none;
+:deep(::-webkit-scrollbar-track) {
+ background: #f1f1f1;
+ border-radius: 3px;
}
-/* 鍗$墖鏍囬鏍峰紡 */
-.el-card__header {
- background: #f8f9fa;
- border-bottom: 1px solid #ebeef5;
- font-weight: 600;
- color: #303133;
+:deep(::-webkit-scrollbar-thumb) {
+ background: #c1c1c1;
+ border-radius: 3px;
+}
+
+:deep(::-webkit-scrollbar-thumb:hover) {
+ background: #a8a8a8;
}
</style>
diff --git a/src/views/OfficeRelated/checkingIn/mockData.js b/src/views/OfficeRelated/checkingIn/mockData.js
index f419c38..8c15455 100644
--- a/src/views/OfficeRelated/checkingIn/mockData.js
+++ b/src/views/OfficeRelated/checkingIn/mockData.js
@@ -1,131 +1,52 @@
-// 鍦ㄧ埗缁勪欢 data() 涓垨鍗曠嫭鍒涘缓 mockData.js 鏂囦欢
export const generateMockData = () => {
return {
- // 鍛樺伐鑰冨嫟鏁版嵁
+ // 鍛樺伐鑰冨嫟鏁版嵁 - 闇�瑕佽ˉ鍏呭畬鏁存湀浠界殑鏁版嵁
attendanceData: [
- {
- id: 1,
- date: '2024-12-01',
- checkIn: '08:30',
- checkOut: '18:00',
- status: 'present',
- workHours: 9.5
- },
- {
- id: 2,
- date: '2024-12-02',
- checkIn: '09:15',
- checkOut: '18:00',
- status: 'late',
- workHours: 8.75
- },
- {
- id: 3,
- date: '2024-12-03',
- checkIn: '08:45',
- checkOut: '17:30',
- status: 'present',
- workHours: 8.75
- },
- {
- id: 4,
- date: '2024-12-04',
- checkIn: '08:25',
- checkOut: '18:10',
- status: 'present',
- workHours: 9.75
- },
- {
- id: 5,
- date: '2024-12-05',
- checkIn: null,
- checkOut: null,
- status: 'absent',
- workHours: 0
- },
- {
- id: 6,
- date: '2024-12-08',
- checkIn: '08:40',
- checkOut: '17:45',
- status: 'present',
- workHours: 9.0
- },
- {
- id: 7,
- date: '2024-12-09',
- checkIn: '08:35',
- checkOut: '18:05',
- status: 'present',
- workHours: 9.5
- },
- {
- id: 8,
- date: '2024-12-10',
- checkIn: '09:05',
- checkOut: '17:50',
- status: 'late',
- workHours: 8.75
- },
- {
- id: 9,
- date: '2024-12-11',
- checkIn: '08:50',
- checkOut: '18:15',
- status: 'present',
- workHours: 9.5
- },
- {
- id: 10,
- date: '2024-12-12',
- checkIn: '08:30',
- checkOut: '17:40',
- status: 'present',
- workHours: 9.0
- },
- {
- id: 11,
- date: '2024-12-15',
- checkIn: '08:45',
- checkOut: '18:00',
- status: 'present',
- workHours: 9.25
- },
- {
- id: 12,
- date: '2024-12-16',
- checkIn: '08:55',
- checkOut: '17:55',
- status: 'present',
- workHours: 9.0
- },
- {
- id: 13,
- date: '2024-12-17',
- checkIn: '08:40',
- checkOut: '18:10',
- status: 'present',
- workHours: 9.5
- },
- {
- id: 14,
- date: '2024-12-18',
- checkIn: '09:20',
- checkOut: '17:30',
- status: 'late',
- workHours: 8.0
- },
- {
- id: 15,
- date: '2024-12-19',
- checkIn: '08:35',
- checkOut: '18:05',
- status: 'present',
- workHours: 9.5
- }
+ // 12鏈�1-4鏃�
+ { id: 1, date: '2024-12-01', checkIn: '08:30', checkOut: '18:00', status: 'present', workHours: 9.5 },
+ { id: 2, date: '2024-12-02', checkIn: '09:15', checkOut: '18:00', status: 'late', workHours: 8.75 },
+ { id: 3, date: '2024-12-03', checkIn: '08:45', checkOut: '17:30', status: 'present', workHours: 8.75 },
+ { id: 4, date: '2024-12-04', checkIn: '08:25', checkOut: '18:10', status: 'present', workHours: 9.75 },
+
+ // 12鏈�5-8鏃ワ紙5鍙风己鍕わ紝5-8鍙峰嚭宸級
+ { id: 5, date: '2024-12-05', checkIn: null, checkOut: null, status: 'absent', workHours: 0 },
+ { id: 6, date: '2024-12-06', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
+ { id: 7, date: '2024-12-07', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
+ { id: 8, date: '2024-12-08', checkIn: '08:40', checkOut: '17:45', status: 'present', workHours: 9.0 },
+
+ // 12鏈�9-14鏃�
+ { id: 9, date: '2024-12-09', checkIn: '08:35', checkOut: '18:05', status: 'present', workHours: 9.5 },
+ { id: 10, date: '2024-12-10', checkIn: '09:05', checkOut: '17:50', status: 'late', workHours: 8.75 },
+ { id: 11, date: '2024-12-11', checkIn: '08:50', checkOut: '18:15', status: 'present', workHours: 9.5 },
+ { id: 12, date: '2024-12-12', checkIn: '08:30', checkOut: '17:40', status: 'present', workHours: 9.0 },
+ { id: 13, date: '2024-12-13', checkIn: '08:55', checkOut: '18:00', status: 'present', workHours: 9.0 },
+ { id: 14, date: '2024-12-14', checkIn: '08:45', checkOut: '17:50', status: 'present', workHours: 9.0 },
+
+ // 12鏈�15-18鏃ワ紙鍑哄樊锛�
+ { id: 15, date: '2024-12-15', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
+ { id: 16, date: '2024-12-16', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
+ { id: 17, date: '2024-12-17', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
+ { id: 18, date: '2024-12-18', checkIn: null, checkOut: null, status: 'trip', workHours: 0 },
+
+ // 12鏈�19-24鏃�
+ { id: 19, date: '2024-12-19', checkIn: '08:35', checkOut: '18:05', status: 'present', workHours: 9.5 },
+ { id: 20, date: '2024-12-20', checkIn: '08:40', checkOut: '17:55', status: 'present', workHours: 9.25 },
+ { id: 21, date: '2024-12-21', checkIn: null, checkOut: null, status: 'absent', workHours: 0 }, // 鍛ㄥ叚
+ { id: 22, date: '2024-12-22', checkIn: null, checkOut: null, status: 'trip', workHours: 0 }, // 鍑哄樊
+ { id: 23, date: '2024-12-23', checkIn: null, checkOut: null, status: 'trip', workHours: 0 }, // 鍑哄樊
+ { id: 24, date: '2024-12-24', checkIn: null, checkOut: null, status: 'trip', workHours: 0 }, // 鍑哄樊
+
+ // 12鏈�25-31鏃�
+ { id: 25, date: '2024-12-25', checkIn: '08:50', checkOut: '17:45', status: 'present', workHours: 8.75 },
+ { id: 26, date: '2024-12-26', checkIn: '09:10', checkOut: '17:40', status: 'late', workHours: 8.5 },
+ { id: 27, date: '2024-12-27', checkIn: '08:35', checkOut: '18:00', status: 'present', workHours: 9.25 },
+ { id: 28, date: '2024-12-28', checkIn: '08:45', checkOut: '17:50', status: 'present', workHours: 9.0 },
+ { id: 29, date: '2024-12-29', checkIn: null, checkOut: null, status: 'absent', workHours: 0 }, // 鍛ㄦ棩
+ { id: 30, date: '2024-12-30', checkIn: '08:30', checkOut: '17:30', status: 'present', workHours: 9.0 },
+ { id: 31, date: '2024-12-31', checkIn: '08:40', checkOut: '16:00', status: 'present', workHours: 7.5 }
],
- // 鍑哄樊鏁版嵁
+ // 鍑哄樊鏁版嵁 - 闇�瑕佽皟鏁存棩鏈熻寖鍥村尮閰�
businessTripData: [
{
id: 1,
@@ -133,7 +54,7 @@
startCity: '鍖椾含',
endCity: '涓婃捣',
startDate: '2024-12-05',
- endDate: '2024-12-08',
+ endDate: '2024-12-07', // 璋冩暣涓�7鍙风粨鏉�
distance: 1200,
purpose: '瀹㈡埛浼氳',
status: 'completed'
diff --git a/src/views/OfficeRelated/conference/index.vue b/src/views/OfficeRelated/conference/index.vue
new file mode 100644
index 0000000..5b87c95
--- /dev/null
+++ b/src/views/OfficeRelated/conference/index.vue
@@ -0,0 +1,728 @@
+<template>
+ <div class="meeting-management">
+ <!-- 椤甸潰澶撮儴 -->
+ <div class="page-header">
+ <h2>浼氳绠$悊</h2>
+ <div class="header-actions">
+ <el-button type="primary" icon="el-icon-plus" @click="handleAdd">
+ 鏂板缓浼氳
+ </el-button>
+ <el-button icon="el-icon-download" @click="exportData">
+ 瀵煎嚭鏁版嵁
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 鎼滅储绛涢�夊尯鍩� -->
+ <el-card class="filter-card">
+ <el-form :model="queryParams" inline>
+ <el-form-item label="浼氳绫诲瀷">
+ <el-select v-model="queryParams.meetingType" clearable placeholder="璇烽�夋嫨">
+ <el-option label="绉戠爺浼氳" value="research" />
+ <el-option label="鏃ュ父浼氳" value="daily" />
+ <el-option label="椤圭洰浼氳" value="project" />
+ <el-option label="閮ㄩ棬浼氳" value="department" />
+ <el-option label="璇勫浼氳" value="review" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浼氳鍦扮偣">
+ <el-input
+ v-model="queryParams.location"
+ placeholder="璇疯緭鍏ヤ細璁湴鐐�"
+ clearable
+ style="width: 150px"
+ />
+ </el-form-item>
+ <el-form-item label="鏃堕棿鑼冨洿">
+ <el-date-picker
+ v-model="queryParams.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="yyyy-MM-dd"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��">
+ <el-select v-model="queryParams.status" clearable placeholder="璇烽�夋嫨">
+ <el-option label="寰呭紑濮�" value="pending" />
+ <el-option label="杩涜涓�" value="ongoing" />
+ <el-option label="宸茬粨鏉�" value="completed" />
+ <el-option label="宸插彇娑�" value="cancelled" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <!-- 鏁版嵁琛ㄦ牸 -->
+ <el-card>
+ <el-table
+ :data="tableData"
+ v-loading="loading"
+ border
+ style="width: 100%"
+ @sort-change="handleSortChange"
+ >
+ <el-table-column prop="id" label="ID" width="80" fixed />
+ <el-table-column prop="title" label="浼氳涓婚" width="200" fixed>
+ <template #default="scope">
+ <el-button type="text" @click="handleView(scope.row)">
+ {{ scope.row.title }}
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column prop="meetingType" label="浼氳绫诲瀷" width="120">
+ <template #default="scope">
+ <el-tag :type="getMeetingTypeTag(scope.row.meetingType)">
+ {{ getMeetingTypeText(scope.row.meetingType) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="participants" label="鍙備細浜哄憳" width="180" show-overflow-tooltip>
+ <template #default="scope">
+ <span>{{ scope.row.participants.join(', ') }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="location" label="浼氳鍦扮偣" width="150" />
+ <el-table-column prop="startTime" label="寮�濮嬫椂闂�" width="160" sortable>
+ <template #default="scope">
+ <span>{{ formatDateTime(scope.row.startTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="endTime" label="缁撴潫鏃堕棿" width="160" sortable>
+ <template #default="scope">
+ <span>{{ formatDateTime(scope.row.endTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="duration" label="鎸佺画鏃堕棿" width="100">
+ <template #default="scope">
+ <span>{{ calculateDuration(scope.row) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="summary" label="浼氳姒傝" min-width="200" show-overflow-tooltip />
+ <el-table-column prop="status" label="鐘舵��" width="100" fixed="right">
+ <template #default="scope">
+ <el-tag :type="getStatusTag(scope.row.status)">
+ {{ getStatusText(scope.row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="200" fixed="right">
+ <template #default="scope">
+ <el-button size="mini" type="text" @click="handleView(scope.row)">
+ 鏌ョ湅
+ </el-button>
+ <el-button size="mini" type="text" @click="handleEdit(scope.row)">
+ 缂栬緫
+ </el-button>
+ <el-button
+ size="mini"
+ type="text"
+ @click="handleCopy(scope.row)"
+ style="color: #67C23A;"
+ >
+ 澶嶅埗
+ </el-button>
+ <el-button
+ size="mini"
+ type="text"
+ @click="handleDelete(scope.row)"
+ style="color: #F56C6C;"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <div class="pagination-container">
+ <el-pagination
+ :current-page="pagination.currentPage"
+ :page-size="pagination.pageSize"
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </el-card>
+
+ <!-- 鏌ョ湅璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ :title="`浼氳璇︽儏 - ${currentRecord.title || ''}`"
+ :visible.sync="detailDialogVisible"
+ width="800px"
+ :before-close="handleDetailClose"
+ >
+ <el-descriptions :column="2" border v-if="currentRecord">
+ <el-descriptions-item label="浼氳涓婚">{{ currentRecord.title }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳绫诲瀷">
+ <el-tag :type="getMeetingTypeTag(currentRecord.meetingType)">
+ {{ getMeetingTypeText(currentRecord.meetingType) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙備細浜哄憳" :span="2">
+ <el-tag
+ v-for="participant in currentRecord.participants"
+ :key="participant"
+ type="info"
+ size="small"
+ style="margin-right: 8px; margin-bottom: 8px;"
+ >
+ {{ participant }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="浼氳鍦扮偣">{{ currentRecord.location }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳鐘舵��">
+ <el-tag :type="getStatusTag(currentRecord.status)">
+ {{ getStatusText(currentRecord.status) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="寮�濮嬫椂闂�">{{ formatDateTime(currentRecord.startTime) }}</el-descriptions-item>
+ <el-descriptions-item label="缁撴潫鏃堕棿">{{ formatDateTime(currentRecord.endTime) }}</el-descriptions-item>
+ <el-descriptions-item label="鎸佺画鏃堕棿">{{ calculateDuration(currentRecord) }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">{{ currentRecord.creator }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳姒傝" :span="2">
+ {{ currentRecord.summary }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浼氳鍐呭" :span="2">
+ <div style="white-space: pre-line; max-height: 300px; overflow-y: auto;">
+ {{ currentRecord.content }}
+ </div>
+ </el-descriptions-item>
+ <el-descriptions-item label="闄勪欢" :span="2" v-if="currentRecord.attachments && currentRecord.attachments.length">
+ <div v-for="file in currentRecord.attachments" :key="file.id" class="attachment-item">
+ <el-link type="primary" :href="file.url" target="_blank">
+ <i class="el-icon-document"></i> {{ file.name }}
+ </el-link>
+ </div>
+ </el-descriptions-item>
+ </el-descriptions>
+ <span slot="footer">
+ <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+ <el-button type="primary" @click="handleEdit(currentRecord)">缂栬緫</el-button>
+ </span>
+ </el-dialog>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog
+ :title="`${isEditing ? '缂栬緫' : '鏂板'}浼氳`"
+ :visible.sync="editDialogVisible"
+ width="700px"
+ :before-close="handleEditClose"
+ >
+ <el-form
+ ref="editForm"
+ :model="editForm"
+ :rules="editRules"
+ label-width="100px"
+ label-position="left"
+ >
+ <el-form-item label="浼氳涓婚" prop="title">
+ <el-input v-model="editForm.title" placeholder="璇疯緭鍏ヤ細璁富棰�" />
+ </el-form-item>
+
+ <el-form-item label="浼氳绫诲瀷" prop="meetingType">
+ <el-select v-model="editForm.meetingType" placeholder="璇烽�夋嫨浼氳绫诲瀷" style="width: 100%">
+ <el-option label="绉戠爺浼氳" value="research" />
+ <el-option label="鏃ュ父浼氳" value="daily" />
+ <el-option label="椤圭洰浼氳" value="project" />
+ <el-option label="閮ㄩ棬浼氳" value="department" />
+ <el-option label="璇勫浼氳" value="review" />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="鍙備細浜哄憳" prop="participants">
+ <el-select
+ v-model="editForm.participants"
+ multiple
+ filterable
+ placeholder="璇烽�夋嫨鍙備細浜哄憳"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.name"
+ :value="user.name"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="浼氳鍦扮偣" prop="location">
+ <el-input v-model="editForm.location" placeholder="璇疯緭鍏ヤ細璁湴鐐�" />
+ </el-form-item>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�濮嬫椂闂�" prop="startTime">
+ <el-date-picker
+ v-model="editForm.startTime"
+ type="datetime"
+ placeholder="閫夋嫨寮�濮嬫椂闂�"
+ value-format="yyyy-MM-dd HH:mm:ss"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁撴潫鏃堕棿" prop="endTime">
+ <el-date-picker
+ v-model="editForm.endTime"
+ type="datetime"
+ placeholder="閫夋嫨缁撴潫鏃堕棿"
+ value-format="yyyy-MM-dd HH:mm:ss"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="浼氳姒傝" prop="summary">
+ <el-input
+ v-model="editForm.summary"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏ヤ細璁瑕�"
+ maxlength="200"
+ show-word-limit
+ />
+ </el-form-item>
+
+ <el-form-item label="浼氳鍐呭" prop="content">
+ <el-input
+ v-model="editForm.content"
+ type="textarea"
+ :rows="6"
+ placeholder="璇疯緭鍏ヤ細璁叿浣撳唴瀹�"
+ />
+ </el-form-item>
+
+ <el-form-item label="闄勪欢涓婁紶">
+ <el-upload
+ action="#"
+ :auto-upload="false"
+ :on-change="handleFileChange"
+ :file-list="editForm.attachments"
+ :limit="5"
+ multiple
+ >
+ <el-button size="small" type="primary">鐐瑰嚮涓婁紶</el-button>
+ <div slot="tip" class="el-upload__tip">鏀寔涓婁紶鏂囨。銆佸浘鐗囩瓑鏂囦欢锛屽崟涓枃浠朵笉瓒呰繃10MB</div>
+ </el-upload>
+ </el-form-item>
+ </el-form>
+
+ <span slot="footer">
+ <el-button @click="handleEditClose">鍙栨秷</el-button>
+ <el-button type="primary" @click="handleSave" :loading="saveLoading">
+ {{ isEditing ? '淇濆瓨' : '鏂板' }}
+ </el-button>
+ </span>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'MeetingManagement',
+ data() {
+ return {
+ // 鏌ヨ鍙傛暟
+ queryParams: {
+ meetingType: '',
+ location: '',
+ dateRange: [],
+ status: ''
+ },
+ // 鍒嗛〉鍙傛暟
+ pagination: {
+ currentPage: 1,
+ pageSize: 10,
+ total: 0
+ },
+ // 鍔犺浇鐘舵��
+ loading: false,
+ saveLoading: false,
+ // 瀵硅瘽妗嗘樉绀虹姸鎬�
+ detailDialogVisible: false,
+ editDialogVisible: false,
+ // 褰撳墠鎿嶄綔璁板綍
+ currentRecord: {},
+ // 缂栬緫鐘舵��
+ isEditing: false,
+ // 琛ㄦ牸鏁版嵁
+ tableData: [],
+ // 鐢ㄦ埛鍒楄〃锛堢敤浜庨�夋嫨鍙備細浜哄憳锛�
+ userList: [
+ { id: 1, name: '寮犱笁' },
+ { id: 2, name: '鏉庡洓' },
+ { id: 3, name: '鐜嬩簲' },
+ { id: 4, name: '璧靛叚' },
+ { id: 5, name: '閽变竷' },
+ { id: 6, name: '瀛欏叓' },
+ { id: 7, name: '鍛ㄤ節' },
+ { id: 8, name: '鍚村崄' }
+ ],
+ // 缂栬緫琛ㄥ崟鏁版嵁
+ editForm: {
+ title: '',
+ meetingType: '',
+ participants: [],
+ location: '',
+ startTime: '',
+ endTime: '',
+ summary: '',
+ content: '',
+ attachments: []
+ },
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ editRules: {
+ title: [{ required: true, message: '璇疯緭鍏ヤ細璁富棰�', trigger: 'blur' }],
+ meetingType: [{ required: true, message: '璇烽�夋嫨浼氳绫诲瀷', trigger: 'change' }],
+ participants: [{ required: true, message: '璇烽�夋嫨鍙備細浜哄憳', trigger: 'change' }],
+ location: [{ required: true, message: '璇疯緭鍏ヤ細璁湴鐐�', trigger: 'blur' }],
+ startTime: [{ required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change' }],
+ endTime: [{ required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change' }],
+ summary: [{ required: true, message: '璇疯緭鍏ヤ細璁瑕�', trigger: 'blur' }],
+ content: [{ required: true, message: '璇疯緭鍏ヤ細璁唴瀹�', trigger: 'blur' }]
+ }
+ }
+ },
+ mounted() {
+ this.loadData()
+ },
+ methods: {
+ // 鍔犺浇鏁版嵁
+ async loadData() {
+ this.loading = true
+ try {
+ // 妯℃嫙API璋冪敤
+ await new Promise(resolve => setTimeout(resolve, 500))
+
+ // 鐢熸垚妯℃嫙鏁版嵁
+ this.tableData = this.generateMockData()
+ this.pagination.total = this.tableData.length
+ } catch (error) {
+ console.error('鍔犺浇鏁版嵁澶辫触:', error)
+ this.$message.error('鏁版嵁鍔犺浇澶辫触')
+ } finally {
+ this.loading = false
+ }
+ },
+
+ // 鐢熸垚妯℃嫙鏁版嵁
+ generateMockData() {
+ const meetingTypes = ['research', 'daily', 'project', 'department', 'review']
+ const statuses = ['pending', 'ongoing', 'completed', 'cancelled']
+ const participantsPool = ['寮犱笁', '鏉庡洓', '鐜嬩簲', '璧靛叚', '閽变竷', '瀛欏叓', '鍛ㄤ節', '鍚村崄']
+
+ return Array.from({ length: 10 }, (_, index) => {
+ const participantCount = Math.floor(Math.random() * 5) + 2
+ const participants = []
+ for (let i = 0; i < participantCount; i++) {
+ const randomIndex = Math.floor(Math.random() * participantsPool.length)
+ participants.push(participantsPool[randomIndex])
+ }
+
+ // 鍘婚噸
+ const uniqueParticipants = [...new Set(participants)]
+
+ const startTime = new Date()
+ startTime.setDate(startTime.getDate() + Math.floor(Math.random() * 30) - 15)
+ startTime.setHours(9 + Math.floor(Math.random() * 8), Math.floor(Math.random() * 4) * 15, 0)
+
+ const endTime = new Date(startTime)
+ endTime.setHours(startTime.getHours() + Math.floor(Math.random() * 3) + 1)
+
+ return {
+ id: index + 1,
+ title: `鍏充簬${['绉戠爺椤圭洰', '鏃ュ父宸ヤ綔', '鎶�鏈瘎瀹�', '閮ㄩ棬鍗忚皟'][Math.floor(Math.random() * 4)]}鐨勪細璁甡,
+ meetingType: meetingTypes[Math.floor(Math.random() * meetingTypes.length)],
+ participants: uniqueParticipants,
+ location: ['绗竴浼氳瀹�', '绗簩浼氳瀹�', '绗笁浼氳瀹�', '绾夸笂浼氳'][Math.floor(Math.random() * 4)],
+ startTime: startTime.toISOString(),
+ endTime: endTime.toISOString(),
+ summary: `鏈浼氳涓昏璁ㄨ${['椤圭洰杩涘睍', '鎶�鏈毦棰�', '宸ヤ綔璁″垝', '闂鍗忚皟'][Math.floor(Math.random() * 4)]}绛夌浉鍏充簨瀹渀,
+ content: `浼氳璇︾粏鍐呭锛歕n1. 璁涓�璁ㄨ\n2. 璁浜屽垎鏋怽n3. 涓嬩竴姝ュ伐浣滆鍒抃n4. 浠诲姟鍒嗛厤`,
+ status: statuses[Math.floor(Math.random() * statuses.length)],
+ creator: '绯荤粺绠$悊鍛�',
+ attachments: []
+ }
+ })
+ },
+
+ // 鑾峰彇浼氳绫诲瀷鏍囩鏍峰紡
+ getMeetingTypeTag(type) {
+ const typeMap = {
+ research: 'primary',
+ daily: 'success',
+ project: 'warning',
+ department: 'info',
+ review: 'danger'
+ }
+ return typeMap[type] || 'info'
+ },
+
+ // 鑾峰彇浼氳绫诲瀷鏂囨湰
+ getMeetingTypeText(type) {
+ const textMap = {
+ research: '绉戠爺浼氳',
+ daily: '鏃ュ父浼氳',
+ project: '椤圭洰浼氳',
+ department: '閮ㄩ棬浼氳',
+ review: '璇勫浼氳'
+ }
+ return textMap[type] || type
+ },
+
+ // 鑾峰彇鐘舵�佹爣绛炬牱寮�
+ getStatusTag(status) {
+ const statusMap = {
+ pending: 'primary',
+ ongoing: 'success',
+ completed: 'info',
+ cancelled: 'danger'
+ }
+ return statusMap[status] || 'info'
+ },
+
+ // 鑾峰彇鐘舵�佹枃鏈�
+ getStatusText(status) {
+ const textMap = {
+ pending: '寰呭紑濮�',
+ ongoing: '杩涜涓�',
+ completed: '宸茬粨鏉�',
+ cancelled: '宸插彇娑�'
+ }
+ return textMap[status] || status
+ },
+
+ // 鏍煎紡鍖栨棩鏈熸椂闂�
+ formatDateTime(dateTime) {
+ if (!dateTime) return ''
+ const date = new Date(dateTime)
+ return date.toLocaleString('zh-CN')
+ },
+
+ // 璁$畻浼氳鎸佺画鏃堕棿
+ calculateDuration(record) {
+ if (!record.startTime || !record.endTime) return ''
+ const start = new Date(record.startTime)
+ const end = new Date(record.endTime)
+ const duration = (end - start) / (1000 * 60) // 鍒嗛挓鏁�
+
+ if (duration < 60) {
+ return `${Math.round(duration)}鍒嗛挓`
+ } else {
+ const hours = Math.floor(duration / 60)
+ const minutes = Math.round(duration % 60)
+ return minutes > 0 ? `${hours}灏忔椂${minutes}鍒嗛挓` : `${hours}灏忔椂`
+ }
+ },
+
+ // 鏌ヨ澶勭悊
+ handleQuery() {
+ this.pagination.currentPage = 1
+ this.loadData()
+ },
+
+ // 閲嶇疆鏌ヨ
+ handleReset() {
+ this.queryParams = {
+ meetingType: '',
+ location: '',
+ dateRange: [],
+ status: ''
+ }
+ this.pagination.currentPage = 1
+ this.loadData()
+ },
+
+ // 鏌ョ湅璇︽儏
+ handleView(record) {
+ this.currentRecord = { ...record }
+ this.detailDialogVisible = true
+ },
+
+ // 鏂板璁板綍
+ handleAdd() {
+ this.isEditing = false
+ this.editForm = this.getDefaultFormData()
+ this.editDialogVisible = true
+ this.$nextTick(() => {
+ this.$refs.editForm && this.$refs.editForm.clearValidate()
+ })
+ },
+
+ // 缂栬緫璁板綍
+ handleEdit(record) {
+ this.isEditing = true
+ this.currentRecord = record
+ this.editForm = { ...record }
+ this.editDialogVisible = true
+ this.detailDialogVisible = false
+ this.$nextTick(() => {
+ this.$refs.editForm && this.$refs.editForm.clearValidate()
+ })
+ },
+
+ // 澶嶅埗璁板綍
+ handleCopy(record) {
+ this.isEditing = false
+ const copiedRecord = { ...record }
+ delete copiedRecord.id
+ copiedRecord.title = copiedRecord.title + '锛堝鍒讹級'
+ this.editForm = copiedRecord
+ this.editDialogVisible = true
+ this.$nextTick(() => {
+ this.$refs.editForm && this.$refs.editForm.clearValidate()
+ })
+ },
+
+ // 鍒犻櫎璁板綍
+ handleDelete(record) {
+ this.$confirm('纭畾瑕佸垹闄よ繖鏉′細璁褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ // 妯℃嫙鍒犻櫎鎿嶄綔
+ this.tableData = this.tableData.filter(item => item.id !== record.id)
+ this.pagination.total = this.tableData.length
+ this.$message.success('鍒犻櫎鎴愬姛')
+ }).catch(() => {})
+ },
+
+ // 淇濆瓨璁板綍
+ async handleSave() {
+ try {
+ const valid = await this.$refs.editForm.validate()
+ if (!valid) return
+
+ this.saveLoading = true
+ // 妯℃嫙API璋冪敤
+ await new Promise(resolve => setTimeout(resolve, 1000))
+
+ this.$message.success(this.isEditing ? '淇濆瓨鎴愬姛' : '鏂板鎴愬姛')
+ this.editDialogVisible = false
+ this.loadData()
+ } catch (error) {
+ console.error('淇濆瓨澶辫触:', error)
+ this.$message.error('鎿嶄綔澶辫触')
+ } finally {
+ this.saveLoading = false
+ }
+ },
+
+ // 鏂囦欢涓婁紶澶勭悊
+ handleFileChange(file, fileList) {
+ this.editForm.attachments = fileList
+ },
+
+ // 鍏抽棴璇︽儏瀵硅瘽妗�
+ handleDetailClose() {
+ this.detailDialogVisible = false
+ this.currentRecord = {}
+ },
+
+ // 鍏抽棴缂栬緫瀵硅瘽妗�
+ handleEditClose() {
+ this.editDialogVisible = false
+ this.currentRecord = {}
+ this.$nextTick(() => {
+ this.$refs.editForm && this.$refs.editForm.clearValidate()
+ })
+ },
+
+ // 瀵煎嚭鏁版嵁
+ exportData() {
+ this.$message.success('瀵煎嚭鍔熻兘寮�鍙戜腑')
+ },
+
+ // 鍒嗛〉澶у皬鍙樺寲
+ handleSizeChange(size) {
+ this.pagination.pageSize = size
+ this.pagination.currentPage = 1
+ this.loadData()
+ },
+
+ // 褰撳墠椤靛彉鍖�
+ handleCurrentChange(page) {
+ this.pagination.currentPage = page
+ this.loadData()
+ },
+
+ // 鎺掑簭鍙樺寲
+ handleSortChange(sort) {
+ console.log('鎺掑簭鍙樺寲:', sort)
+ },
+
+ // 鑾峰彇榛樿琛ㄥ崟鏁版嵁
+ getDefaultFormData() {
+ return {
+ title: '',
+ meetingType: '',
+ participants: [],
+ location: '',
+ startTime: '',
+ endTime: '',
+ summary: '',
+ content: '',
+ attachments: []
+ }
+ }
+ }
+}
+</script>
+
+<style scoped>
+.meeting-management {
+ padding: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.filter-card {
+ margin-bottom: 20px;
+}
+
+.pagination-container {
+ margin-top: 20px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.attachment-item {
+ margin-bottom: 8px;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 768px) {
+ .page-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ }
+
+ .header-actions {
+ width: 100%;
+ justify-content: space-between;
+ }
+}
+</style>
diff --git a/src/views/OfficeRelated/engage/index.vue b/src/views/OfficeRelated/engage/index.vue
new file mode 100644
index 0000000..21c2dd2
--- /dev/null
+++ b/src/views/OfficeRelated/engage/index.vue
@@ -0,0 +1,691 @@
+<template>
+ <div class="training-management">
+ <!-- 椤甸潰澶撮儴 -->
+ <div class="page-header">
+ <h2>杩涗慨绠$悊</h2>
+ <div class="header-actions">
+ <el-button type="primary" icon="el-icon-plus" @click="handleAdd">
+ 鏂板杩涗慨璁板綍
+ </el-button>
+ <el-button icon="el-icon-download" @click="exportData">
+ 瀵煎嚭鏁版嵁
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 鎼滅储绛涢�夊尯鍩� -->
+ <el-card class="filter-card">
+ <el-form :model="queryParams" inline>
+ <el-form-item label="濮撳悕">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ鍚�"
+ clearable
+ style="width: 120px"
+ />
+ </el-form-item>
+ <el-form-item label="杩涗慨绫诲瀷">
+ <el-select v-model="queryParams.trainingType" clearable placeholder="璇烽�夋嫨">
+ <el-option label="涓撲笟鎶�鏈繘淇�" value="professional" />
+ <el-option label="绠$悊鑳藉姏杩涗慨" value="management" />
+ <el-option label="瀛﹀巻鎻愬崌杩涗慨" value="education" />
+ <el-option label="鎶�鑳藉煿璁�" value="skill" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎶�鏈亴绉�">
+ <el-input
+ v-model="queryParams.technicalTitle"
+ placeholder="璇疯緭鍏ユ妧鏈亴绉�"
+ clearable
+ style="width: 140px"
+ />
+ </el-form-item>
+ <el-form-item label="杩涗慨鏃堕棿">
+ <el-date-picker
+ v-model="queryParams.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="yyyy-MM-dd"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <!-- 鏁版嵁琛ㄦ牸 -->
+ <el-card>
+ <el-table
+ :data="tableData"
+ v-loading="loading"
+ border
+ style="width: 100%"
+ @sort-change="handleSortChange"
+ >
+ <el-table-column prop="id" label="ID" width="80" fixed />
+ <el-table-column prop="name" label="濮撳悕" width="100" fixed>
+ <template #default="scope">
+ <el-button type="text" @click="handleView(scope.row)">
+ {{ scope.row.name }}
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column prop="gender" label="鎬у埆" width="80">
+ <template #default="scope">
+ <el-tag :type="scope.row.gender === '鐢�' ? 'primary' : 'danger'" size="small">
+ {{ scope.row.gender }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="age" label="骞撮緞" width="80" sortable />
+ <el-table-column prop="education" label="瀛﹀巻" width="100" />
+ <el-table-column prop="trainingType" label="杩涗慨绫诲瀷" width="120">
+ <template #default="scope">
+ <el-tag :type="getTrainingTypeTag(scope.row.trainingType)">
+ {{ getTrainingTypeText(scope.row.trainingType) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="idCard" label="韬唤璇佸彿" width="180" show-overflow-tooltip />
+ <el-table-column prop="graduateSchool" label="姣曚笟闄㈡牎" width="150" show-overflow-tooltip />
+ <el-table-column prop="workUnit" label="鎵�鍦ㄥ崟浣�" width="150" show-overflow-tooltip />
+ <el-table-column prop="technicalTitle" label="鎶�鏈亴绉�" width="120" />
+ <el-table-column prop="professionalField" label="浠庝簨涓撲笟" width="120" />
+ <el-table-column prop="workYears" label="宸ヤ綔骞撮檺" width="100" sortable />
+ <el-table-column prop="trainingStartDate" label="杩涗慨寮�濮嬫椂闂�" width="120" />
+ <el-table-column prop="trainingEndDate" label="杩涗慨缁撴潫鏃堕棿" width="120" />
+ <el-table-column prop="trainingMajor" label="杩涗慨涓撲笟" width="120" />
+ <el-table-column prop="status" label="鐘舵��" width="100" fixed="right">
+ <template #default="scope">
+ <el-tag :type="getStatusTag(scope.row.status)">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="180" fixed="right">
+ <template #default="scope">
+ <el-button size="mini" type="text" @click="handleView(scope.row)">
+ 鏌ョ湅
+ </el-button>
+ <el-button size="mini" type="text" @click="handleEdit(scope.row)">
+ 缂栬緫
+ </el-button>
+ <el-button
+ size="mini"
+ type="text"
+ @click="handleCopy(scope.row)"
+ style="color: #67C23A;"
+ >
+ 澶嶅埗
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <div class="pagination-container">
+ <el-pagination
+ :current-page="pagination.currentPage"
+ :page-size="pagination.pageSize"
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </el-card>
+
+ <!-- 鏌ョ湅璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ :title="`杩涗慨璇︽儏 - ${currentRecord.name || ''}`"
+ :visible.sync="detailDialogVisible"
+ width="900px"
+ :before-close="handleDetailClose"
+ >
+ <el-descriptions :column="2" border v-if="currentRecord">
+ <el-descriptions-item label="濮撳悕">{{ currentRecord.name }}</el-descriptions-item>
+ <el-descriptions-item label="鎬у埆">{{ currentRecord.gender }}</el-descriptions-item>
+ <el-descriptions-item label="骞撮緞">{{ currentRecord.age }}</el-descriptions-item>
+ <el-descriptions-item label="瀛﹀巻">{{ currentRecord.education }}</el-descriptions-item>
+ <el-descriptions-item label="杩涗慨绫诲瀷">
+ <el-tag :type="getTrainingTypeTag(currentRecord.trainingType)">
+ {{ getTrainingTypeText(currentRecord.trainingType) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="韬唤璇佸彿">{{ currentRecord.idCard }}</el-descriptions-item>
+ <el-descriptions-item label="姣曚笟闄㈡牎">{{ currentRecord.graduateSchool }}</el-descriptions-item>
+ <el-descriptions-item label="鎵�鍦ㄥ崟浣�">{{ currentRecord.workUnit }}</el-descriptions-item>
+ <el-descriptions-item label="鎶�鏈亴绉�">{{ currentRecord.technicalTitle }}</el-descriptions-item>
+ <el-descriptions-item label="浠庝簨涓撲笟">{{ currentRecord.professionalField }}</el-descriptions-item>
+ <el-descriptions-item label="宸ヤ綔骞撮檺">{{ currentRecord.workYears }}骞�</el-descriptions-item>
+ <el-descriptions-item label="杩涗慨鐩爣">{{ currentRecord.trainingObjective }}</el-descriptions-item>
+ <el-descriptions-item label="杩涗慨寮�濮嬫椂闂�">{{ currentRecord.trainingStartDate }}</el-descriptions-item>
+ <el-descriptions-item label="杩涗慨缁撴潫鏃堕棿">{{ currentRecord.trainingEndDate }}</el-descriptions-item>
+ <el-descriptions-item label="杩涗慨涓撲笟">{{ currentRecord.trainingMajor }}</el-descriptions-item>
+ <el-descriptions-item label="浠庝簨宸ヤ綔鎯呭喌" :span="2">
+ {{ currentRecord.workSituation }}
+ </el-descriptions-item>
+ <el-descriptions-item label="杩涗慨绉戠洰鍙婄洰鐨�" :span="2">
+ {{ currentRecord.trainingSubject }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涓昏瀛﹀巻" :span="2">
+ <div style="white-space: pre-line;">{{ currentRecord.mainEducation }}</div>
+ </el-descriptions-item>
+ <el-descriptions-item label="涓昏宸ヤ綔缁忓巻" :span="2">
+ <div style="white-space: pre-line;">{{ currentRecord.workExperience }}</div>
+ </el-descriptions-item>
+ </el-descriptions>
+ <span slot="footer">
+ <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+ <el-button type="primary" @click="handleEdit(currentRecord)">缂栬緫</el-button>
+ </span>
+ </el-dialog>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog
+ :title="`${isEditing ? '缂栬緫' : '鏂板'}杩涗慨璁板綍`"
+ :visible.sync="editDialogVisible"
+ width="80vw"
+ :before-close="handleEditClose"
+ >
+ <el-form
+ ref="editForm"
+ :model="editForm"
+ :rules="editRules"
+ label-width="120px"
+ label-position="left"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="濮撳悕" prop="name">
+ <el-input v-model="editForm.name" placeholder="璇疯緭鍏ュ鍚�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎬у埆" prop="gender">
+ <el-radio-group v-model="editForm.gender">
+ <el-radio label="鐢�">鐢�</el-radio>
+ <el-radio label="濂�">濂�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="骞撮緞" prop="age">
+ <el-input-number
+ v-model="editForm.age"
+ :min="18"
+ :max="65"
+ controls-position="right"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀛﹀巻" prop="education">
+ <el-select v-model="editForm.education" placeholder="璇烽�夋嫨瀛﹀巻" style="width: 100%">
+ <el-option label="鍗氬+" value="鍗氬+" />
+ <el-option label="纭曞+" value="纭曞+" />
+ <el-option label="鏈" value="鏈" />
+ <el-option label="澶т笓" value="澶т笓" />
+ <el-option label="涓笓" value="涓笓" />
+ <el-option label="楂樹腑" value="楂樹腑" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="杩涗慨绫诲瀷" prop="trainingType">
+ <el-select v-model="editForm.trainingType" placeholder="璇烽�夋嫨杩涗慨绫诲瀷" style="width: 100%">
+ <el-option label="涓撲笟鎶�鏈繘淇�" value="professional" />
+ <el-option label="绠$悊鑳藉姏杩涗慨" value="management" />
+ <el-option label="瀛﹀巻鎻愬崌杩涗慨" value="education" />
+ <el-option label="鎶�鑳藉煿璁�" value="skill" />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="韬唤璇佸彿" prop="idCard">
+ <el-input v-model="editForm.idCard" placeholder="璇疯緭鍏ヨ韩浠借瘉鍙�" />
+ </el-form-item>
+
+ <el-form-item label="姣曚笟闄㈡牎" prop="graduateSchool">
+ <el-input v-model="editForm.graduateSchool" placeholder="璇疯緭鍏ユ瘯涓氶櫌鏍�" />
+ </el-form-item>
+
+ <el-form-item label="鎵�鍦ㄥ崟浣�" prop="workUnit">
+ <el-input v-model="editForm.workUnit" placeholder="璇疯緭鍏ユ墍鍦ㄥ崟浣�" />
+ </el-form-item>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鎶�鏈亴绉�" prop="technicalTitle">
+ <el-input v-model="editForm.technicalTitle" placeholder="璇疯緭鍏ユ妧鏈亴绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠庝簨涓撲笟" prop="professionalField">
+ <el-input v-model="editForm.professionalField" placeholder="璇疯緭鍏ヤ粠浜嬩笓涓�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="宸ヤ綔骞撮檺" prop="workYears">
+ <el-input-number
+ v-model="editForm.workYears"
+ :min="0"
+ :max="50"
+ controls-position="right"
+ style="width: 100%"
+ />
+ </el-form-item>
+
+ <el-form-item label="杩涗慨鐩爣" prop="trainingObjective">
+ <el-input
+ v-model="editForm.trainingObjective"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏ヨ繘淇洰鏍�"
+ />
+ </el-form-item>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="杩涗慨寮�濮嬫椂闂�" prop="trainingStartDate">
+ <el-date-picker
+ v-model="editForm.trainingStartDate"
+ type="date"
+ placeholder="閫夋嫨寮�濮嬫棩鏈�"
+ value-format="yyyy-MM-dd"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="杩涗慨缁撴潫鏃堕棿" prop="trainingEndDate">
+ <el-date-picker
+ v-model="editForm.trainingEndDate"
+ type="date"
+ placeholder="閫夋嫨缁撴潫鏃ユ湡"
+ value-format="yyyy-MM-dd"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="杩涗慨涓撲笟" prop="trainingMajor">
+ <el-input v-model="editForm.trainingMajor" placeholder="璇疯緭鍏ヨ繘淇笓涓�" />
+ </el-form-item>
+
+ <el-form-item label="浠庝簨宸ヤ綔鎯呭喌" prop="workSituation">
+ <el-input
+ v-model="editForm.workSituation"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヤ粠浜嬪伐浣滄儏鍐�"
+ />
+ </el-form-item>
+
+ <el-form-item label="杩涗慨绉戠洰鍙婄洰鐨�" prop="trainingSubject">
+ <el-input
+ v-model="editForm.trainingSubject"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ユ湰娆$敵璇疯繘淇綍绉嶇鐩強鐩殑瑕佹眰"
+ />
+ </el-form-item>
+
+ <el-form-item label="涓昏瀛﹀巻" prop="mainEducation">
+ <el-input
+ v-model="editForm.mainEducation"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヤ富瑕佸鍘嗭紙姣忚涓�鏉★級"
+ />
+ </el-form-item>
+
+ <el-form-item label="涓昏宸ヤ綔缁忓巻" prop="workExperience">
+ <el-input
+ v-model="editForm.workExperience"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヤ富瑕佸伐浣滅粡鍘嗭紙姣忚涓�鏉★級"
+ />
+ </el-form-item>
+ </el-form>
+
+ <span slot="footer">
+ <el-button @click="handleEditClose">鍙栨秷</el-button>
+ <el-button type="primary" @click="handleSave" :loading="saveLoading">
+ {{ isEditing ? '淇濆瓨' : '鏂板' }}
+ </el-button>
+ </span>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'TrainingManagement',
+ data() {
+ return {
+ // 鏌ヨ鍙傛暟
+ queryParams: {
+ name: '',
+ trainingType: '',
+ technicalTitle: '',
+ dateRange: []
+ },
+ // 鍒嗛〉鍙傛暟
+ pagination: {
+ currentPage: 1,
+ pageSize: 10,
+ total: 0
+ },
+ // 鍔犺浇鐘舵��
+ loading: false,
+ saveLoading: false,
+ // 瀵硅瘽妗嗘樉绀虹姸鎬�
+ detailDialogVisible: false,
+ editDialogVisible: false,
+ // 褰撳墠鎿嶄綔璁板綍
+ currentRecord: {},
+ // 缂栬緫鐘舵��
+ isEditing: false,
+ // 琛ㄦ牸鏁版嵁
+ tableData: [],
+ // 缂栬緫琛ㄥ崟鏁版嵁
+ editForm: {
+ name: '',
+ gender: '鐢�',
+ age: 25,
+ education: '',
+ trainingType: '',
+ idCard: '',
+ graduateSchool: '',
+ workUnit: '',
+ technicalTitle: '',
+ professionalField: '',
+ workYears: 0,
+ trainingObjective: '',
+ trainingStartDate: '',
+ trainingEndDate: '',
+ trainingMajor: '',
+ workSituation: '',
+ trainingSubject: '',
+ mainEducation: '',
+ workExperience: ''
+ },
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ editRules: {
+ name: [{ required: true, message: '璇疯緭鍏ュ鍚�', trigger: 'blur' }],
+ gender: [{ required: true, message: '璇烽�夋嫨鎬у埆', trigger: 'change' }],
+ age: [{ required: true, message: '璇疯緭鍏ュ勾榫�', trigger: 'blur' }],
+ education: [{ required: true, message: '璇烽�夋嫨瀛﹀巻', trigger: 'change' }],
+ trainingType: [{ required: true, message: '璇烽�夋嫨杩涗慨绫诲瀷', trigger: 'change' }],
+ idCard: [{ required: true, message: '璇疯緭鍏ヨ韩浠借瘉鍙�', trigger: 'blur' }]
+ }
+ }
+ },
+ mounted() {
+ this.loadData()
+ },
+ methods: {
+ // 鍔犺浇鏁版嵁
+ async loadData() {
+ this.loading = true
+ try {
+ // 妯℃嫙API璋冪敤
+ await new Promise(resolve => setTimeout(resolve, 500))
+
+ // 鐢熸垚妯℃嫙鏁版嵁
+ this.tableData = this.generateMockData()
+ this.pagination.total = this.tableData.length
+ } catch (error) {
+ console.error('鍔犺浇鏁版嵁澶辫触:', error)
+ this.$message.error('鏁版嵁鍔犺浇澶辫触')
+ } finally {
+ this.loading = false
+ }
+ },
+ // 鐢熸垚妯℃嫙鏁版嵁
+ generateMockData() {
+ const names = ['寮犱笁', '鏉庡洓', '鐜嬩簲', '璧靛叚', '閽变竷', '瀛欏叓', '鍛ㄤ節', '鍚村崄']
+ const trainingTypes = ['professional', 'management', 'education', 'skill']
+ const statuses = ['杩涜涓�', '宸插畬鎴�', '寰呭鏍�', '宸插彇娑�']
+
+ return Array.from({ length: 10 }, (_, index) => ({
+ id: index + 1,
+ name: names[Math.floor(Math.random() * names.length)],
+ gender: Math.random() > 0.5 ? '鐢�' : '濂�',
+ age: 25 + Math.floor(Math.random() * 20),
+ education: ['鍗氬+', '纭曞+', '鏈', '澶т笓'][Math.floor(Math.random() * 4)],
+ trainingType: trainingTypes[Math.floor(Math.random() * trainingTypes.length)],
+ idCard: '11010119900101' + (1000 + index).toString().slice(-4),
+ graduateSchool: ['鍖椾含澶у', '娓呭崕澶у', '澶嶆棪澶у', '涓婃捣浜ら�氬ぇ瀛�'][Math.floor(Math.random() * 4)],
+ workUnit: ['鍖椾含鍗忓拰鍖婚櫌', '涓婃捣鐟為噾鍖婚櫌', '骞垮窞涓北鍖婚櫌', '姝︽眽鍚屾祹鍖婚櫌'][Math.floor(Math.random() * 4)],
+ technicalTitle: ['涓讳换鍖诲笀', '鍓富浠诲尰甯�', '涓绘不鍖诲笀', '浣忛櫌鍖诲笀'][Math.floor(Math.random() * 4)],
+ professionalField: ['蹇冭绠″唴绉�', '绁炵粡澶栫', '鍎跨', '濡囦骇绉�'][Math.floor(Math.random() * 4)],
+ workYears: 5 + Math.floor(Math.random() * 20),
+ trainingObjective: '鎻愬崌涓撲笟鎶�鏈拰涓村簥鑳藉姏',
+ trainingStartDate: '2024-' + (Math.floor(Math.random() * 12) + 1).toString().padStart(2, '0') + '-01',
+ trainingEndDate: '2024-' + (Math.floor(Math.random() * 12) + 1).toString().padStart(2, '0') + '-28',
+ trainingMajor: '涓村簥鍖诲',
+ workSituation: '鍦ㄤ复搴婁竴绾垮伐浣滐紝璐熻矗鎮h�呰瘖鐤�',
+ 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>
--
Gitblit v1.9.3