¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container self-service-container"> |
| | | |
| | | <!-- åè½å¯¼èªå¡ç --> |
| | | <el-row :gutter="20" class="nav-cards"> |
| | | <el-col :span="6" v-for="(item, index) in navItems" :key="index"> |
| | | <el-card class="nav-card" @click="handleNavClick(item.type)"> |
| | | <div class="nav-content"> |
| | | <el-icon :size="40" class="nav-icon"> |
| | | <component :is="item.icon" /> |
| | | </el-icon> |
| | | <h3>{{ item.title }}</h3> |
| | | <p>{{ item.desc }}</p> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 主è¦å
容åºå --> |
| | | <div class="main-content"> |
| | | <!-- èå¤è®°å½ --> |
| | | <el-card v-if="currentView === 'attendance'" class="content-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>个人èå¤è®°å½</span> |
| | | <el-button type="primary" @click="addAttendanceRecord">æ°å¢è®°å½</el-button> |
| | | </div> |
| | | </template> |
| | | <el-table :data="attendanceData" style="width: 100%"> |
| | | <el-table-column prop="date" label="æ¥æ" /> |
| | | <el-table-column prop="checkIn" label="ç¾å°æ¶é´" /> |
| | | <el-table-column prop="checkOut" label="ç¾éæ¶é´" /> |
| | | <el-table-column prop="workHours" label="工使¶é¿" width="100" /> |
| | | <el-table-column prop="status" label="ç¶æ" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.status === 'æ£å¸¸' ? 'success' : 'danger'"> |
| | | {{ scope.row.status }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="150"> |
| | | <template #default="scope"> |
| | | <el-button size="small" @click="editAttendanceRecord(scope.row)">ç¼è¾</el-button> |
| | | <el-button size="small" type="danger" @click="deleteAttendanceRecord(scope.$index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <!-- èªèµå --> |
| | | <el-card v-if="currentView === 'salary'" class="content-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>èªèµåæ¥è¯¢</span> |
| | | <el-date-picker v-model="salaryMonth" type="month" placeholder="éæ©æä»½" /> |
| | | </div> |
| | | </template> |
| | | <el-table :data="salaryData" style="width: 100%"> |
| | | <el-table-column prop="month" label="æä»½" /> |
| | | <el-table-column prop="basicSalary" label="åºæ¬å·¥èµ" /> |
| | | <el-table-column prop="bonus" label="å¥é" /> |
| | | <el-table-column prop="deduction" label="æ£æ¬¾" /> |
| | | <el-table-column prop="total" label="å®åå·¥èµ" /> |
| | | <el-table-column prop="status" label="ç¶æ" > |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.status === '已忾' ? 'success' : 'warning'"> |
| | | {{ scope.row.status }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <!-- åæç³è¯· --> |
| | | <el-card v-if="currentView === 'leave'" class="content-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>åæç³è¯·ç®¡ç</span> |
| | | <el-button type="primary" @click="showLeaveDialog = true">ç³è¯·åæ</el-button> |
| | | </div> |
| | | </template> |
| | | <el-table :data="leaveData" style="width: 100%"> |
| | | <el-table-column prop="type" label="åæç±»å" /> |
| | | <el-table-column prop="startDate" label="å¼å§æ¥æ" /> |
| | | <el-table-column prop="endDate" label="ç»ææ¥æ" /> |
| | | <el-table-column prop="days" label="天æ°" width="80" /> |
| | | <el-table-column prop="reason" label="ç³è¯·åå " /> |
| | | <el-table-column prop="status" label="审æ¹ç¶æ" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getStatusType(scope.row.status)"> |
| | | {{ scope.row.status }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="150"> |
| | | <template #default="scope"> |
| | | <el-button size="small" @click="editLeaveRecord(scope.row)">ç¼è¾</el-button> |
| | | <el-button size="small" type="danger" @click="deleteLeaveRecord(scope.$index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <!-- ä¸ªäººä¿¡æ¯ --> |
| | | <el-card v-if="currentView === 'profile'" class="content-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>个人信æ¯ç»´æ¤</span> |
| | | <el-button type="primary" @click="editProfile = true">ç¼è¾ä¿¡æ¯</el-button> |
| | | </div> |
| | | </template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="å§å">{{ profile.name }}</el-descriptions-item> |
| | | <el-descriptions-item label="å·¥å·">{{ profile.employeeId }}</el-descriptions-item> |
| | | <el-descriptions-item label="é¨é¨">{{ profile.department }}</el-descriptions-item> |
| | | <el-descriptions-item label="èä½">{{ profile.position }}</el-descriptions-item> |
| | | <el-descriptions-item label="å
¥èæ¥æ">{{ profile.hireDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="èç³»çµè¯">{{ profile.phone }}</el-descriptions-item> |
| | | <el-descriptions-item label="é®ç®±">{{ profile.email }}</el-descriptions-item> |
| | | <el-descriptions-item label="å°å">{{ profile.address }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <!-- åæç³è¯·å¼¹çª --> |
| | | <el-dialog v-model="showLeaveDialog" title="ç³è¯·åæ" width="500px"> |
| | | <el-form :model="leaveForm" label-width="100px"> |
| | | <el-form-item label="åæç±»å"> |
| | | <el-select v-model="leaveForm.type" placeholder="è¯·éæ©åæç±»å"> |
| | | <el-option label="å¹´å" value="å¹´å" /> |
| | | <el-option label="ç
å" value="ç
å" /> |
| | | <el-option label="è°ä¼" value="è°ä¼" /> |
| | | <el-option label="äºå" value="äºå" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="å¼å§æ¥æ"> |
| | | <el-date-picker v-model="leaveForm.startDate" type="date" placeholder="éæ©å¼å§æ¥æ" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç»ææ¥æ"> |
| | | <el-date-picker v-model="leaveForm.endDate" type="date" placeholder="éæ©ç»ææ¥æ" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç³è¯·åå "> |
| | | <el-input v-model="leaveForm.reason" type="textarea" rows="3" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="showLeaveDialog = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitLeaveApplication">æäº¤ç³è¯·</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- æ°å¢èå¤è®°å½å¼¹çª --> |
| | | <el-dialog v-model="showAttendanceDialog" title="æ°å¢èå¤è®°å½" width="500px"> |
| | | <el-form :model="attendanceForm" :rules="attendanceRules" ref="attendanceFormRef" label-width="100px"> |
| | | <el-form-item label="æ¥æ" prop="date"> |
| | | <el-date-picker v-model="attendanceForm.date" type="date" placeholder="éæ©æ¥æ" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¾å°æ¶é´" prop="checkIn"> |
| | | <el-time-picker v-model="attendanceForm.checkIn" placeholder="éæ©ç¾å°æ¶é´" format="HH:mm" value-format="HH:mm" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¾éæ¶é´" prop="checkOut"> |
| | | <el-time-picker v-model="attendanceForm.checkOut" placeholder="éæ©ç¾éæ¶é´" format="HH:mm" value-format="HH:mm" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-select v-model="attendanceForm.status" placeholder="è¯·éæ©ç¶æ"> |
| | | <el-option label="æ£å¸¸" value="æ£å¸¸" /> |
| | | <el-option label="è¿å°" value="è¿å°" /> |
| | | <el-option label="æ©é" value="æ©é" /> |
| | | <el-option label="缺å¤" value="缺å¤" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="showAttendanceDialog = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitAttendance">æäº¤</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 个人信æ¯ç¼è¾å¼¹çª --> |
| | | <el-dialog v-model="editProfile" title="ç¼è¾ä¸ªäººä¿¡æ¯" width="500px"> |
| | | <el-form :model="profileForm" label-width="100px"> |
| | | <el-form-item label="å§å"> |
| | | <el-input v-model="profileForm.name" /> |
| | | </el-form-item> |
| | | <el-form-item label="èç³»çµè¯"> |
| | | <el-input v-model="profileForm.phone" /> |
| | | </el-form-item> |
| | | <el-form-item label="é®ç®±"> |
| | | <el-input v-model="profileForm.email" /> |
| | | </el-form-item> |
| | | <el-form-item label="å°å"> |
| | | <el-input v-model="profileForm.address" type="textarea" rows="2" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="editProfile = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="saveProfile">ä¿å</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { |
| | | Calendar, |
| | | Money, |
| | | Clock, |
| | | User |
| | | } from '@element-plus/icons-vue' |
| | | |
| | | // å½åè§å¾ |
| | | const currentView = ref('attendance') |
| | | |
| | | // 导èªé¡¹ |
| | | const navItems = [ |
| | | { type: 'attendance', title: 'èå¤è®°å½', desc: 'æ¥è¯¢ä¸ªäººèå¤ä¿¡æ¯', icon: 'Calendar' }, |
| | | { type: 'salary', title: 'èªèµå', desc: 'æ¥çèªèµåæ¾è®°å½', icon: 'Money' }, |
| | | { type: 'leave', title: 'åæç³è¯·', desc: 'å¨çº¿ç³è¯·åç±»åæ', icon: 'Clock' }, |
| | | { type: 'profile', title: '个人信æ¯', desc: 'ç»´æ¤ä¸ªäººåºæ¬ä¿¡æ¯', icon: 'User' } |
| | | ] |
| | | |
| | | // è夿°æ® |
| | | const attendanceData = ref([ |
| | | { date: '2024-01-15', checkIn: '09:00', checkOut: '18:00', workHours: '9å°æ¶', status: 'æ£å¸¸' }, |
| | | { date: '2024-01-16', checkIn: '08:55', checkOut: '18:05', workHours: '9å°æ¶10å', status: 'æ£å¸¸' }, |
| | | { date: '2024-01-17', checkIn: '09:15', checkOut: '18:00', workHours: '8å°æ¶45å', status: 'è¿å°' } |
| | | ]) |
| | | |
| | | // èªèµæ°æ® |
| | | const salaryData = ref([ |
| | | { month: '2024-01', basicSalary: 8000, bonus: 1000, deduction: 200, total: 8800, status: '已忾' }, |
| | | { month: '2023-12', basicSalary: 8000, bonus: 800, deduction: 150, total: 8650, status: '已忾' } |
| | | ]) |
| | | |
| | | // åææ°æ® |
| | | const leaveData = ref([ |
| | | { type: 'å¹´å', startDate: '2024-02-01', endDate: '2024-02-03', days: 3, reason: 'æ¥èåå®¶', status: 'å·²éè¿' }, |
| | | { type: 'ç
å', startDate: '2024-01-20', endDate: '2024-01-21', days: 2, reason: 'æååç§', status: '审æ¹ä¸' } |
| | | ]) |
| | | |
| | | // ä¸ªäººä¿¡æ¯ |
| | | const profile = ref({ |
| | | name: 'å¼ æµ·æ´', |
| | | employeeId: 'EMP001', |
| | | department: 'ææ¯é¨', |
| | | position: '软件工ç¨å¸', |
| | | hireDate: '2023-03-01', |
| | | phone: '13800138000', |
| | | email: 'zhangsan@company.com', |
| | | address: 'åäº¬å¸æé³åºxxxè¡éxxxå·' |
| | | }) |
| | | |
| | | // å¼¹çªæ§å¶ |
| | | const showLeaveDialog = ref(false) |
| | | const editProfile = ref(false) |
| | | const salaryMonth = ref('') |
| | | |
| | | // è¡¨åæ°æ® |
| | | const leaveForm = reactive({ |
| | | type: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | reason: '' |
| | | }) |
| | | |
| | | const profileForm = reactive({ |
| | | name: '', |
| | | phone: '', |
| | | email: '', |
| | | address: '' |
| | | }) |
| | | |
| | | // æ°å¢èå¤è®°å½ï¼å¼¹çªä¸è¡¨å |
| | | const showAttendanceDialog = ref(false) |
| | | const attendanceFormRef = ref(null) |
| | | const attendanceForm = reactive({ |
| | | date: '', |
| | | checkIn: '', |
| | | checkOut: '', |
| | | status: 'æ£å¸¸' |
| | | }) |
| | | const attendanceRules = { |
| | | date: [{ required: true, message: 'è¯·éæ©æ¥æ', trigger: 'change' }], |
| | | checkIn: [{ required: true, message: 'è¯·éæ©ç¾å°æ¶é´', trigger: 'change' }], |
| | | checkOut: [{ required: true, message: 'è¯·éæ©ç¾éæ¶é´', trigger: 'change' }], |
| | | status: [{ required: true, message: 'è¯·éæ©ç¶æ', trigger: 'change' }] |
| | | } |
| | | |
| | | // å¤ç导èªç¹å» |
| | | const handleNavClick = (type) => { |
| | | currentView.value = type |
| | | } |
| | | |
| | | // è·åç¶æç±»å |
| | | const getStatusType = (status) => { |
| | | const types = { |
| | | 'å·²éè¿': 'success', |
| | | '审æ¹ä¸': 'warning', |
| | | 'å·²æç»': 'danger' |
| | | } |
| | | return types[status] || 'info' |
| | | } |
| | | |
| | | // æ°å¢èå¤è®°å½ï¼æå¼å¼¹çªå¹¶é¢å¡«é»è®¤å¼ï¼ |
| | | const addAttendanceRecord = () => { |
| | | attendanceForm.date = new Date().toISOString().split('T')[0] |
| | | attendanceForm.checkIn = '09:00' |
| | | attendanceForm.checkOut = '18:00' |
| | | attendanceForm.status = 'æ£å¸¸' |
| | | showAttendanceDialog.value = true |
| | | } |
| | | |
| | | // 计ç®å·¥æ¶ |
| | | const computeWorkHours = (inStr, outStr) => { |
| | | const [inH, inM] = inStr.split(':').map(n => parseInt(n, 10)) |
| | | const [outH, outM] = outStr.split(':').map(n => parseInt(n, 10)) |
| | | const inMin = inH * 60 + inM |
| | | const outMin = outH * 60 + outM |
| | | const diff = Math.max(0, outMin - inMin) |
| | | const h = Math.floor(diff / 60) |
| | | const m = diff % 60 |
| | | return m === 0 ? `${h}å°æ¶` : `${h}å°æ¶${m}å` |
| | | } |
| | | |
| | | // æäº¤æ°å¢èå¤è®°å½ |
| | | const submitAttendance = () => { |
| | | if (!attendanceFormRef.value) return |
| | | attendanceFormRef.value.validate((valid) => { |
| | | if (!valid) return |
| | | const workHours = computeWorkHours(attendanceForm.checkIn, attendanceForm.checkOut) |
| | | const newRecord = { |
| | | date: attendanceForm.date, |
| | | checkIn: attendanceForm.checkIn, |
| | | checkOut: attendanceForm.checkOut, |
| | | workHours, |
| | | status: attendanceForm.status |
| | | } |
| | | attendanceData.value.unshift(newRecord) |
| | | showAttendanceDialog.value = false |
| | | // é置表å |
| | | attendanceForm.date = '' |
| | | attendanceForm.checkIn = '' |
| | | attendanceForm.checkOut = '' |
| | | attendanceForm.status = 'æ£å¸¸' |
| | | ElMessage.success('èå¤è®°å½æ·»å æå') |
| | | }) |
| | | } |
| | | |
| | | // ç¼è¾èå¤è®°å½ |
| | | const editAttendanceRecord = (row) => { |
| | | ElMessage.info('ç¼è¾åè½å¼åä¸...') |
| | | } |
| | | |
| | | // å é¤èå¤è®°å½ |
| | | const deleteAttendanceRecord = (index) => { |
| | | attendanceData.value.splice(index, 1) |
| | | ElMessage.success('èå¤è®°å½å 餿å') |
| | | } |
| | | |
| | | // ç¼è¾åæè®°å½ |
| | | const editLeaveRecord = (row) => { |
| | | ElMessage.info('ç¼è¾åè½å¼åä¸...') |
| | | } |
| | | |
| | | // å é¤åæè®°å½ |
| | | const deleteLeaveRecord = (index) => { |
| | | leaveData.value.splice(index, 1) |
| | | ElMessage.success('åæè®°å½å 餿å') |
| | | } |
| | | |
| | | // æäº¤åæç³è¯· |
| | | const submitLeaveApplication = () => { |
| | | if (!leaveForm.type || !leaveForm.startDate || !leaveForm.endDate || !leaveForm.reason) { |
| | | ElMessage.warning('请填å宿´ä¿¡æ¯') |
| | | return |
| | | } |
| | | |
| | | const newLeave = { |
| | | type: leaveForm.type, |
| | | startDate: leaveForm.startDate, |
| | | endDate: leaveForm.endDate, |
| | | days: 3, // ç®åè®¡ç® |
| | | reason: leaveForm.reason, |
| | | status: '审æ¹ä¸' |
| | | } |
| | | |
| | | leaveData.value.unshift(newLeave) |
| | | showLeaveDialog.value = false |
| | | |
| | | // é置表å |
| | | Object.keys(leaveForm).forEach(key => { |
| | | leaveForm[key] = '' |
| | | }) |
| | | |
| | | ElMessage.success('åæç³è¯·æäº¤æå') |
| | | } |
| | | |
| | | // ä¿åä¸ªäººä¿¡æ¯ |
| | | const saveProfile = () => { |
| | | Object.assign(profile.value, profileForm) |
| | | editProfile.value = false |
| | | ElMessage.success('个人信æ¯ä¿åæå') |
| | | } |
| | | |
| | | // åå§å个人信æ¯è¡¨å |
| | | const initProfileForm = () => { |
| | | Object.assign(profileForm, { |
| | | name: profile.value.name, |
| | | phone: profile.value.phone, |
| | | email: profile.value.email, |
| | | address: profile.value.address |
| | | }) |
| | | } |
| | | |
| | | // çå¬ç¼è¾ä¸ªäººä¿¡æ¯å¼¹çª |
| | | watch(editProfile, (val) => { |
| | | if (val) { |
| | | initProfileForm() |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .self-service-container { |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .page-header { |
| | | text-align: center; |
| | | margin-bottom: 30px; |
| | | padding: 20px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | border-radius: 12px; |
| | | color: white; |
| | | } |
| | | |
| | | .page-header h2 { |
| | | color: white; |
| | | margin-bottom: 10px; |
| | | font-size: 28px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .page-header p { |
| | | color: rgba(255, 255, 255, 0.9); |
| | | font-size: 14px; |
| | | margin: 0; |
| | | } |
| | | |
| | | .nav-cards { |
| | | margin-bottom: 30px; |
| | | } |
| | | |
| | | .nav-card { |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | border-radius: 12px; |
| | | border: none; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .nav-card:hover { |
| | | transform: translateY(-5px); |
| | | box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); |
| | | } |
| | | |
| | | .nav-content { |
| | | text-align: center; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .nav-icon { |
| | | color: #409EFF; |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .nav-content h3 { |
| | | margin: 0 0 10px 0; |
| | | color: #303133; |
| | | font-size: 18px; |
| | | } |
| | | |
| | | .nav-content p { |
| | | margin: 0; |
| | | color: #909399; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .main-content { |
| | | margin-bottom: 30px; |
| | | } |
| | | |
| | | .content-card { |
| | | border-radius: 12px; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | border: none; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | /* ååºå¼è®¾è®¡ */ |
| | | @media (max-width: 768px) { |
| | | .self-service-container { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .nav-cards .el-col { |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .page-header h2 { |
| | | font-size: 24px; |
| | | } |
| | | } |
| | | </style> |