<template>
|
<div class="personal-attendance-table">
|
<div class="table-header">
|
<h4>出勤记录详情</h4>
|
<div class="header-actions">
|
<el-button size="small" @click="exportToCSV" icon="el-icon-download">
|
导出CSV
|
</el-button>
|
<el-tooltip content="刷新数据" placement="top">
|
<el-button size="small" @click="refreshData" icon="el-icon-refresh">
|
刷新
|
</el-button>
|
</el-tooltip>
|
</div>
|
</div>
|
|
<el-table
|
:data="filteredData"
|
border
|
style="width: 100%"
|
v-loading="loading"
|
class="attendance-table"
|
>
|
<el-table-column prop="date" label="日期" sortable>
|
<template #default="scope">
|
<span class="date-cell">{{ scope.row.date }}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="checkIn" label="签到时间" >
|
<template #default="scope">
|
<span :class="getTimeClass(scope.row.checkIn, 'checkIn')">
|
{{ scope.row.checkIn || '-' }}
|
</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="checkOut" label="签退时间" >
|
<template #default="scope">
|
<span :class="getTimeClass(scope.row.checkOut, 'checkOut')">
|
{{ scope.row.checkOut || '-' }}
|
</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="workHours" label="工作时长" sortable>
|
<template #default="scope">
|
<el-tag
|
:type="getWorkHoursType(scope.row.workHours)"
|
size="small"
|
>
|
{{ scope.row.workHours }}h
|
</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="status" label="状态" >
|
<template #default="scope">
|
<el-tag
|
:type="getStatusType(scope.row.status)"
|
effect="plain"
|
>
|
{{ scope.row.status }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="remarks" label="备注" min-width="150" show-overflow-tooltip>
|
<template #default="scope">
|
<span class="remarks-cell">{{ scope.row.remarks || '无' }}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="操作" fixed="right">
|
<template #default="scope">
|
<el-button
|
size="mini"
|
type="text"
|
@click="viewDetails(scope.row)"
|
icon="el-icon-view"
|
>
|
详情
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<!-- 分页 -->
|
<div class="pagination-container">
|
<el-pagination
|
:current-page="currentPage"
|
:page-size="pageSize"
|
:total="total"
|
layout="total, sizes, prev, pager, next, jumper"
|
@size-change="handleSizeChange"
|
@current-change="handleCurrentChange"
|
/>
|
</div>
|
|
<!-- 详情对话框 -->
|
<el-dialog
|
title="出勤记录详情"
|
:visible.sync="detailDialogVisible"
|
width="500px"
|
>
|
<div v-if="currentRecord" class="record-details">
|
<el-descriptions :column="1" border>
|
<el-descriptions-item label="日期">
|
{{ currentRecord.date }}
|
</el-descriptions-item>
|
<el-descriptions-item label="签到时间">
|
<el-tag :type="getTimeClass(currentRecord.checkIn, 'checkIn')">
|
{{ currentRecord.checkIn }}
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="签退时间">
|
<el-tag :type="getTimeClass(currentRecord.checkOut, 'checkOut')">
|
{{ currentRecord.checkOut }}
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="工作时长">
|
<el-tag :type="getWorkHoursType(currentRecord.workHours)">
|
{{ currentRecord.workHours }} 小时
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="状态">
|
<el-tag :type="getStatusType(currentRecord.status)">
|
{{ currentRecord.status }}
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="备注">
|
{{ currentRecord.remarks || '无' }}
|
</el-descriptions-item>
|
</el-descriptions>
|
</div>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
export default {
|
name: 'PersonalAttendanceTable',
|
props: {
|
data: {
|
type: Array,
|
default: () => []
|
},
|
loading: {
|
type: Boolean,
|
default: false
|
}
|
},
|
data() {
|
return {
|
currentPage: 1,
|
pageSize: 10,
|
detailDialogVisible: false,
|
currentRecord: null,
|
filterForm: {
|
dateRange: [],
|
status: ''
|
}
|
}
|
},
|
computed: {
|
total() {
|
return this.data.length
|
},
|
filteredData() {
|
let filtered = this.data
|
|
// 按日期范围过滤
|
if (this.filterForm.dateRange && this.filterForm.dateRange.length === 2) {
|
const [start, end] = this.filterForm.dateRange
|
filtered = filtered.filter(item => {
|
const itemDate = new Date(item.date)
|
return itemDate >= new Date(start) && itemDate <= new Date(end)
|
})
|
}
|
|
// 按状态过滤
|
if (this.filterForm.status) {
|
filtered = filtered.filter(item => item.status === this.filterForm.status)
|
}
|
|
// 分页
|
const start = (this.currentPage - 1) * this.pageSize
|
const end = start + this.pageSize
|
return filtered.slice(start, end)
|
}
|
},
|
methods: {
|
getTimeClass(time, type) {
|
if (!time) return 'text-muted'
|
|
const hour = parseInt(time.split(':')[0])
|
if (type === 'checkIn') {
|
return hour > 9 ? 'text-danger' : 'text-success'
|
} else {
|
return hour < 18 ? 'text-warning' : 'text-success'
|
}
|
},
|
|
getWorkHoursType(hours) {
|
const numHours = parseFloat(hours)
|
if (numHours >= 8) return 'success'
|
if (numHours >= 6) return 'warning'
|
return 'danger'
|
},
|
|
getStatusType(status) {
|
const typeMap = {
|
'正常': 'success',
|
'迟到': 'warning',
|
'早退': 'warning',
|
'缺勤': 'danger',
|
'出差': 'primary'
|
}
|
return typeMap[status] || 'info'
|
},
|
|
viewDetails(record) {
|
this.currentRecord = record
|
this.detailDialogVisible = true
|
},
|
|
exportToCSV() {
|
// 简化版CSV导出逻辑
|
const headers = ['日期', '签到时间', '签退时间', '工作时长', '状态', '备注']
|
const csvData = this.data.map(item => [
|
item.date,
|
item.checkIn,
|
item.checkOut,
|
item.workHours,
|
item.status,
|
item.remarks || ''
|
])
|
|
const csvContent = [headers, ...csvData]
|
.map(row => row.map(field => `"${field}"`).join(','))
|
.join('\n')
|
|
const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' })
|
const link = document.createElement('a')
|
link.href = URL.createObjectURL(blob)
|
link.download = `出勤记录_${new Date().toISOString().split('T')[0]}.csv`
|
link.click()
|
},
|
|
refreshData() {
|
this.$emit('refresh')
|
},
|
|
handleSizeChange(size) {
|
this.pageSize = size
|
this.currentPage = 1
|
},
|
|
handleCurrentChange(page) {
|
this.currentPage = page
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.personal-attendance-table {
|
padding: 20px;
|
}
|
|
.table-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 20px;
|
}
|
|
.table-header h4 {
|
margin: 0;
|
color: #303133;
|
font-size: 16px;
|
}
|
|
.header-actions {
|
display: flex;
|
gap: 10px;
|
}
|
|
.attendance-table {
|
margin-bottom: 20px;
|
}
|
|
.date-cell {
|
font-weight: 500;
|
}
|
|
.text-success { color: #67c23a; }
|
.text-warning { color: #e6a23c; }
|
.text-danger { color: #f56c6c; }
|
.text-muted { color: #909399; }
|
.text-primary { color: #409eff; }
|
|
.remarks-cell {
|
color: #606266;
|
font-size: 13px;
|
}
|
|
.pagination-container {
|
display: flex;
|
justify-content: flex-end;
|
margin-top: 20px;
|
}
|
|
.record-details {
|
padding: 10px;
|
}
|
|
/* 响应式设计 */
|
@media (max-width: 768px) {
|
.table-header {
|
flex-direction: column;
|
align-items: flex-start;
|
gap: 10px;
|
}
|
|
.header-actions {
|
width: 100%;
|
justify-content: flex-end;
|
}
|
}
|
</style>
|