|
|
<!DOCTYPE html>
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>人员管理 - 智慧河道管理平台</title>
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
<!-- <link rel="stylesheet" href="styles.css"> -->
|
|
|
<link rel="stylesheet" href="all.min.css">
|
|
|
<script src="chart.js"></script>
|
|
|
<style>
|
|
|
/* ==== 全局样式 ==== */
|
|
|
body {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
font-family: 'Arial', sans-serif;
|
|
|
background-color: #f5f9ff;
|
|
|
color: #333;
|
|
|
}
|
|
|
|
|
|
/* ==== 主容器 ==== */
|
|
|
.main-container {
|
|
|
display: flex;
|
|
|
min-height: 100vh;
|
|
|
padding: 20px;
|
|
|
box-sizing: border-box;
|
|
|
gap: 20px;
|
|
|
}
|
|
|
|
|
|
/* ==== 左侧内容区域 ==== */
|
|
|
.content-area {
|
|
|
flex: 1;
|
|
|
background: #ffffff;
|
|
|
border-radius: 12px;
|
|
|
box-shadow: 0 4px 20px rgba(0, 75, 150, 0.1);
|
|
|
padding: 20px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
/* ==== 右侧侧边栏 ==== */
|
|
|
.sidebar-area {
|
|
|
width: 350px;
|
|
|
background: #ffffff;
|
|
|
border-radius: 12px;
|
|
|
box-shadow: 0 4px 20px rgba(0, 75, 150, 0.1);
|
|
|
padding: 20px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
overflow-y: auto;
|
|
|
max-height: calc(100vh - 40px);
|
|
|
}
|
|
|
|
|
|
/* ==== 头部区域 ==== */
|
|
|
.personnel-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 20px;
|
|
|
padding-bottom: 15px;
|
|
|
border-bottom: 1px solid #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.personnel-title {
|
|
|
color: #1a4b8c;
|
|
|
font-size: 24px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.personnel-actions {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
.action-btn {
|
|
|
background: linear-gradient(135deg, #2a7de1, #1a4b8c);
|
|
|
color: #fff;
|
|
|
border: none;
|
|
|
padding: 8px 16px;
|
|
|
border-radius: 6px;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.3s ease;
|
|
|
font-size: 14px;
|
|
|
box-shadow: 0 2px 8px rgba(42, 125, 225, 0.2);
|
|
|
}
|
|
|
|
|
|
.action-btn:hover {
|
|
|
background: linear-gradient(135deg, #1a4b8c, #2a7de1);
|
|
|
transform: translateY(-2px);
|
|
|
box-shadow: 0 4px 12px rgba(42, 125, 225, 0.3);
|
|
|
}
|
|
|
|
|
|
/* ==== 标签页 ==== */
|
|
|
.personnel-tabs {
|
|
|
display: flex;
|
|
|
background: #e1e9f5;
|
|
|
border-radius: 10px;
|
|
|
padding: 5px;
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
.tab-btn {
|
|
|
flex: 1;
|
|
|
padding: 12px 20px;
|
|
|
background: transparent;
|
|
|
color: #4a6fa5;
|
|
|
border: none;
|
|
|
border-radius: 8px;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.3s ease;
|
|
|
font-size: 14px;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.tab-btn.active {
|
|
|
background: #ffffff;
|
|
|
color: #1a4b8c;
|
|
|
font-weight: 600;
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
.tab-content {
|
|
|
display: none;
|
|
|
flex: 1;
|
|
|
overflow-y: auto;
|
|
|
padding-right: 5px;
|
|
|
}
|
|
|
|
|
|
.tab-content.active {
|
|
|
display: block;
|
|
|
animation: fadeIn 0.5s ease;
|
|
|
}
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
from {
|
|
|
opacity: 0;
|
|
|
transform: translateY(10px);
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
opacity: 1;
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ==== 卡片样式 ==== */
|
|
|
.personnel-card {
|
|
|
background: #ffffff;
|
|
|
border-radius: 12px;
|
|
|
padding: 20px;
|
|
|
margin-bottom: 20px;
|
|
|
border: 1px solid #e1e9f5;
|
|
|
box-shadow: 0 2px 10px rgba(0, 75, 150, 0.05);
|
|
|
}
|
|
|
|
|
|
.card-title {
|
|
|
color: #1a4b8c;
|
|
|
font-size: 18px;
|
|
|
font-weight: 600;
|
|
|
margin-bottom: 15px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
.card-title i {
|
|
|
color: #2a7de1;
|
|
|
}
|
|
|
|
|
|
/* ==== 表单样式 ==== */
|
|
|
.form-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
gap: 15px;
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
.form-group {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.form-group label {
|
|
|
color: #4a6fa5;
|
|
|
font-size: 14px;
|
|
|
margin-bottom: 5px;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.form-group input,
|
|
|
.form-group select,
|
|
|
.form-group textarea {
|
|
|
background: #f5f9ff;
|
|
|
border: 1px solid #d1e0f5;
|
|
|
border-radius: 6px;
|
|
|
padding: 10px;
|
|
|
color: #1a4b8c;
|
|
|
font-size: 14px;
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.form-group input:focus,
|
|
|
.form-group select:focus,
|
|
|
.form-group textarea:focus {
|
|
|
border-color: #2a7de1;
|
|
|
box-shadow: 0 0 0 3px rgba(42, 125, 225, 0.2);
|
|
|
outline: none;
|
|
|
}
|
|
|
|
|
|
/* ==== 表格样式 ==== */
|
|
|
.personnel-table {
|
|
|
width: 100%;
|
|
|
background: #ffffff;
|
|
|
border-radius: 10px;
|
|
|
overflow: hidden;
|
|
|
border-collapse: separate;
|
|
|
border-spacing: 0;
|
|
|
box-shadow: 0 2px 10px rgba(0, 75, 150, 0.05);
|
|
|
}
|
|
|
|
|
|
.personnel-table th,
|
|
|
.personnel-table td {
|
|
|
padding: 12px 15px;
|
|
|
text-align: left;
|
|
|
border-bottom: 1px solid #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.personnel-table th {
|
|
|
background: #f5f9ff;
|
|
|
color: #1a4b8c;
|
|
|
font-weight: 600;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.personnel-table td {
|
|
|
color: #4a6fa5;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
|
|
|
.personnel-table tr:last-child td {
|
|
|
border-bottom: none;
|
|
|
}
|
|
|
|
|
|
/* ==== 状态标签 ==== */
|
|
|
.status-badge {
|
|
|
padding: 4px 8px;
|
|
|
border-radius: 12px;
|
|
|
font-size: 12px;
|
|
|
font-weight: 500;
|
|
|
text-align: center;
|
|
|
display: inline-block;
|
|
|
min-width: 60px;
|
|
|
}
|
|
|
|
|
|
.status-online {
|
|
|
background: rgba(40, 167, 69, 0.1);
|
|
|
color: #28a745;
|
|
|
border: 1px solid rgba(40, 167, 69, 0.3);
|
|
|
}
|
|
|
|
|
|
.status-offline {
|
|
|
background: rgba(220, 53, 69, 0.1);
|
|
|
color: #dc3545;
|
|
|
border: 1px solid rgba(220, 53, 69, 0.3);
|
|
|
}
|
|
|
|
|
|
.status-normal {
|
|
|
background: rgba(40, 167, 69, 0.1);
|
|
|
color: #28a745;
|
|
|
border: 1px solid rgba(40, 167, 69, 0.3);
|
|
|
}
|
|
|
|
|
|
.status-late {
|
|
|
background: rgba(255, 193, 7, 0.1);
|
|
|
color: #ffc107;
|
|
|
border: 1px solid rgba(255, 193, 7, 0.3);
|
|
|
}
|
|
|
|
|
|
.status-absent {
|
|
|
background: rgba(220, 53, 69, 0.1);
|
|
|
color: #dc3545;
|
|
|
border: 1px solid rgba(220, 53, 69, 0.3);
|
|
|
}
|
|
|
|
|
|
/* ==== 侧边栏样式 ==== */
|
|
|
.sidebar-section {
|
|
|
background: #f5f9ff;
|
|
|
border-radius: 12px;
|
|
|
padding: 15px;
|
|
|
margin-bottom: 20px;
|
|
|
border: 1px solid #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.sidebar-title {
|
|
|
color: #1a4b8c;
|
|
|
font-size: 16px;
|
|
|
font-weight: 600;
|
|
|
margin-bottom: 15px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
.sidebar-title i {
|
|
|
color: #2a7de1;
|
|
|
}
|
|
|
|
|
|
/* ==== 统计卡片 ==== */
|
|
|
.stats-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
gap: 10px;
|
|
|
margin-bottom: 15px;
|
|
|
}
|
|
|
|
|
|
.stat-item {
|
|
|
background: #ffffff;
|
|
|
padding: 15px;
|
|
|
border-radius: 8px;
|
|
|
text-align: center;
|
|
|
box-shadow: 0 2px 8px rgba(0, 75, 150, 0.05);
|
|
|
border: 1px solid #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.stat-value {
|
|
|
color: #2a7de1;
|
|
|
font-size: 24px;
|
|
|
font-weight: 700;
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
.stat-label {
|
|
|
color: #4a6fa5;
|
|
|
font-size: 12px;
|
|
|
margin-top: 5px;
|
|
|
}
|
|
|
|
|
|
/* ==== 图表容器 ==== */
|
|
|
.chart-container {
|
|
|
background: #ffffff;
|
|
|
border-radius: 8px;
|
|
|
padding: 15px;
|
|
|
height: 200px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
color: #a8c0e0;
|
|
|
border: 1px solid #e1e9f5;
|
|
|
box-shadow: 0 2px 8px rgba(0, 75, 150, 0.05);
|
|
|
}
|
|
|
|
|
|
/* ==== 人员网格 ==== */
|
|
|
.personnel-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
|
gap: 20px;
|
|
|
}
|
|
|
|
|
|
.personnel-item {
|
|
|
background: #ffffff;
|
|
|
border-radius: 12px;
|
|
|
padding: 20px;
|
|
|
border: 1px solid #e1e9f5;
|
|
|
transition: all 0.3s ease;
|
|
|
box-shadow: 0 2px 8px rgba(0, 75, 150, 0.05);
|
|
|
}
|
|
|
|
|
|
.personnel-item:hover {
|
|
|
transform: translateY(-5px);
|
|
|
box-shadow: 0 8px 20px rgba(0, 75, 150, 0.1);
|
|
|
}
|
|
|
|
|
|
.personnel-info {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 15px;
|
|
|
margin-bottom: 15px;
|
|
|
}
|
|
|
|
|
|
.personnel-icon {
|
|
|
width: 50px;
|
|
|
height: 50px;
|
|
|
background: linear-gradient(135deg, #e1e9f5, #d1e0f5);
|
|
|
border-radius: 10px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
font-size: 24px;
|
|
|
color: #2a7de1;
|
|
|
}
|
|
|
|
|
|
.personnel-details h4 {
|
|
|
color: #1a4b8c;
|
|
|
margin: 0 0 5px 0;
|
|
|
font-size: 16px;
|
|
|
}
|
|
|
|
|
|
.personnel-details p {
|
|
|
color: #4a6fa5;
|
|
|
margin: 0;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.personnel-params {
|
|
|
display: grid;
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
gap: 10px;
|
|
|
margin-bottom: 15px;
|
|
|
}
|
|
|
|
|
|
.param-item {
|
|
|
background: #f5f9ff;
|
|
|
padding: 8px;
|
|
|
border-radius: 6px;
|
|
|
text-align: center;
|
|
|
border: 1px solid #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.param-value {
|
|
|
color: #2a7de1;
|
|
|
font-weight: 600;
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
.param-label {
|
|
|
color: #4a6fa5;
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
|
|
|
/* ==== 告警列表 ==== */
|
|
|
.alert-list {
|
|
|
max-height: 300px;
|
|
|
overflow-y: auto;
|
|
|
}
|
|
|
|
|
|
.alert-item {
|
|
|
background: #ffffff;
|
|
|
padding: 12px;
|
|
|
border-radius: 8px;
|
|
|
margin-bottom: 10px;
|
|
|
border-left: 4px solid #dc3545;
|
|
|
box-shadow: 0 2px 8px rgba(0, 75, 150, 0.05);
|
|
|
}
|
|
|
|
|
|
.alert-title {
|
|
|
color: #1a4b8c;
|
|
|
font-size: 14px;
|
|
|
font-weight: 600;
|
|
|
margin-bottom: 5px;
|
|
|
}
|
|
|
|
|
|
.alert-info {
|
|
|
color: #4a6fa5;
|
|
|
font-size: 12px;
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
|
|
|
/* ==== 按钮样式 ==== */
|
|
|
.btn {
|
|
|
background: #f5f9ff;
|
|
|
color: #2a7de1;
|
|
|
border: 1px solid #d1e0f5;
|
|
|
padding: 8px 16px;
|
|
|
border-radius: 6px;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.3s ease;
|
|
|
font-size: 12px;
|
|
|
margin-right: 5px;
|
|
|
}
|
|
|
|
|
|
.btn:hover {
|
|
|
background: #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
background: rgba(42, 125, 225, 0.1);
|
|
|
color: #2a7de1;
|
|
|
border-color: rgba(42, 125, 225, 0.3);
|
|
|
}
|
|
|
|
|
|
.btn-primary:hover {
|
|
|
background: rgba(42, 125, 225, 0.2);
|
|
|
}
|
|
|
|
|
|
.btn-success {
|
|
|
background: rgba(40, 167, 69, 0.1);
|
|
|
color: #28a745;
|
|
|
border-color: rgba(40, 167, 69, 0.3);
|
|
|
}
|
|
|
|
|
|
.btn-success:hover {
|
|
|
background: rgba(40, 167, 69, 0.2);
|
|
|
}
|
|
|
|
|
|
.btn-danger {
|
|
|
background: rgba(220, 53, 69, 0.1);
|
|
|
color: #dc3545;
|
|
|
border-color: rgba(220, 53, 69, 0.3);
|
|
|
}
|
|
|
|
|
|
.btn-danger:hover {
|
|
|
background: rgba(220, 53, 69, 0.2);
|
|
|
}
|
|
|
|
|
|
.btn-warning {
|
|
|
background: rgba(255, 193, 7, 0.1);
|
|
|
color: #ffc107;
|
|
|
border-color: rgba(255, 193, 7, 0.3);
|
|
|
}
|
|
|
|
|
|
.btn-warning:hover {
|
|
|
background: rgba(255, 193, 7, 0.2);
|
|
|
}
|
|
|
|
|
|
/* ==== 模态框 ==== */
|
|
|
.modal {
|
|
|
display: none;
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
z-index: 1000;
|
|
|
backdrop-filter: blur(5px);
|
|
|
}
|
|
|
|
|
|
.modal-content {
|
|
|
background: #ffffff;
|
|
|
margin: 5% auto;
|
|
|
padding: 30px;
|
|
|
border-radius: 15px;
|
|
|
width: 80%;
|
|
|
max-width: 800px;
|
|
|
max-height: 80vh;
|
|
|
overflow-y: auto;
|
|
|
box-shadow: 0 10px 30px rgba(0, 75, 150, 0.2);
|
|
|
}
|
|
|
|
|
|
.modal h2 {
|
|
|
color: #1a4b8c;
|
|
|
margin-bottom: 20px;
|
|
|
font-size: 20px;
|
|
|
border-bottom: 1px solid #e1e9f5;
|
|
|
padding-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
.close {
|
|
|
color: #4a6fa5;
|
|
|
float: right;
|
|
|
font-size: 28px;
|
|
|
font-weight: bold;
|
|
|
cursor: pointer;
|
|
|
margin-top: -10px;
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.close:hover {
|
|
|
color: #2a7de1;
|
|
|
}
|
|
|
|
|
|
/* ==== 滚动条样式 ==== */
|
|
|
::-webkit-scrollbar {
|
|
|
width: 8px;
|
|
|
height: 8px;
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-track {
|
|
|
background: #f5f9ff;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
background: #d1e0f5;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
|
background: #a8c0e0;
|
|
|
}
|
|
|
|
|
|
/* ==== 新增模态框样式 ==== */
|
|
|
.modal-overlay {
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
z-index: 1000;
|
|
|
backdrop-filter: blur(5px);
|
|
|
}
|
|
|
|
|
|
.modal-overlay .modal-content {
|
|
|
background: #ffffff;
|
|
|
border-radius: 12px;
|
|
|
padding: 25px;
|
|
|
width: 90%;
|
|
|
max-width: 700px;
|
|
|
max-height: 90vh;
|
|
|
overflow-y: auto;
|
|
|
box-shadow: 0 10px 30px rgba(0, 75, 150, 0.2);
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.modal-overlay .modal-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 20px;
|
|
|
padding-bottom: 15px;
|
|
|
border-bottom: 1px solid #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.modal-overlay .modal-header h3 {
|
|
|
color: #1a4b8c;
|
|
|
font-size: 18px;
|
|
|
margin: 0;
|
|
|
}
|
|
|
|
|
|
.modal-overlay .modal-close {
|
|
|
color: #4a6fa5;
|
|
|
font-size: 24px;
|
|
|
font-weight: bold;
|
|
|
cursor: pointer;
|
|
|
background: none;
|
|
|
border: none;
|
|
|
padding: 0;
|
|
|
margin: 0;
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.modal-overlay .modal-close:hover {
|
|
|
color: #2a7de1;
|
|
|
transform: scale(1.1);
|
|
|
}
|
|
|
|
|
|
.modal-overlay .modal-body {
|
|
|
padding: 10px 0;
|
|
|
}
|
|
|
|
|
|
/* ==== 新增排班日历样式 ==== */
|
|
|
|
|
|
.schedule-calendar {
|
|
|
background: #f5f9ff;
|
|
|
border-radius: 12px;
|
|
|
padding: 20px;
|
|
|
margin-top: 20px;
|
|
|
border: 1px solid #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.calendar-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
.calendar-header h3 {
|
|
|
color: #1a4b8c;
|
|
|
margin: 0;
|
|
|
font-size: 16px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.calendar-nav {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
.calendar-nav-btn {
|
|
|
background: #e1e9f5;
|
|
|
border: none;
|
|
|
border-radius: 6px;
|
|
|
padding: 8px 12px;
|
|
|
cursor: pointer;
|
|
|
color: #1a4b8c;
|
|
|
font-size: 14px;
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.calendar-nav-btn:hover {
|
|
|
background: #d1e0f5;
|
|
|
}
|
|
|
|
|
|
.calendar-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(7, 1fr);
|
|
|
gap: 1px;
|
|
|
background: #e1e9f5;
|
|
|
border-radius: 8px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.calendar-day {
|
|
|
background: white;
|
|
|
padding: 10px;
|
|
|
min-height: 80px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.calendar-day.header {
|
|
|
background: #f5f9ff;
|
|
|
color: #1a4b8c;
|
|
|
font-weight: 600;
|
|
|
text-align: center;
|
|
|
min-height: 40px;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
.calendar-day.today {
|
|
|
background: rgba(255, 193, 7, 0.1);
|
|
|
border: 1px solid rgba(255, 193, 7, 0.3);
|
|
|
}
|
|
|
|
|
|
.calendar-day.scheduled {
|
|
|
background: rgba(40, 167, 69, 0.1);
|
|
|
border: 1px solid rgba(40, 167, 69, 0.3);
|
|
|
}
|
|
|
|
|
|
.day-number {
|
|
|
font-weight: 600;
|
|
|
margin-bottom: 5px;
|
|
|
color: #1a4b8c;
|
|
|
}
|
|
|
|
|
|
.day-staff {
|
|
|
font-size: 11px;
|
|
|
color: #4a6fa5;
|
|
|
flex: 1;
|
|
|
overflow-y: auto;
|
|
|
}
|
|
|
|
|
|
.day-staff-item {
|
|
|
margin-bottom: 3px;
|
|
|
padding: 2px 5px;
|
|
|
background: rgba(42, 125, 225, 0.1);
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
/* 更新日历样式 */
|
|
|
.schedule-calendar {
|
|
|
background: #ffffff;
|
|
|
border-radius: 12px;
|
|
|
padding: 20px;
|
|
|
margin-top: 20px;
|
|
|
border: 1px solid #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.calendar-day {
|
|
|
background: white;
|
|
|
padding: 10px;
|
|
|
min-height: 100px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
border: 1px solid #e1e9f5;
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.calendar-day.header {
|
|
|
background: #f5f9ff;
|
|
|
color: #1a4b8c;
|
|
|
font-weight: 600;
|
|
|
text-align: center;
|
|
|
min-height: 40px;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
.calendar-day.today {
|
|
|
background: rgba(255, 193, 7, 0.1);
|
|
|
border: 2px solid #ffc107;
|
|
|
}
|
|
|
|
|
|
.calendar-day.scheduled {
|
|
|
background: rgba(42, 125, 225, 0.05);
|
|
|
}
|
|
|
|
|
|
.calendar-day.scheduled .day-number {
|
|
|
color: #2a7de1;
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
|
|
|
.day-number {
|
|
|
font-weight: 600;
|
|
|
margin-bottom: 5px;
|
|
|
color: #4a6fa5;
|
|
|
align-self: flex-end;
|
|
|
}
|
|
|
|
|
|
.day-staff {
|
|
|
font-size: 11px;
|
|
|
color: #4a6fa5;
|
|
|
flex: 1;
|
|
|
overflow-y: auto;
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.day-staff-item {
|
|
|
margin-bottom: 3px;
|
|
|
padding: 3px 5px;
|
|
|
background: rgba(42, 125, 225, 0.1);
|
|
|
border-radius: 4px;
|
|
|
color: #1a4b8c;
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
}
|
|
|
|
|
|
.day-staff-item i {
|
|
|
margin-right: 3px;
|
|
|
color: #2a7de1;
|
|
|
}
|
|
|
|
|
|
.calendar-day.other-month {
|
|
|
background: #f9f9f9;
|
|
|
color: #a8c0e0;
|
|
|
}
|
|
|
|
|
|
.calendar-day.other-month .day-number {
|
|
|
color: #a8c0e0;
|
|
|
}
|
|
|
|
|
|
/* 网格管理按钮样式 */
|
|
|
.grid-actions {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
/* 网格管理模态框样式 */
|
|
|
.grid-modal-content {
|
|
|
width: 800px;
|
|
|
max-width: 90vw;
|
|
|
}
|
|
|
|
|
|
.grid-table {
|
|
|
width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
margin-top: 15px;
|
|
|
}
|
|
|
|
|
|
.grid-table th,
|
|
|
.grid-table td {
|
|
|
padding: 12px;
|
|
|
text-align: left;
|
|
|
border-bottom: 1px solid #e1e9f5;
|
|
|
}
|
|
|
|
|
|
.grid-table th {
|
|
|
background: #f5f9ff;
|
|
|
color: #1a4b8c;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
/* 新增网格表单样式 */
|
|
|
.grid-form .form-group {
|
|
|
margin-bottom: 15px;
|
|
|
}
|
|
|
|
|
|
.grid-form label {
|
|
|
display: block;
|
|
|
margin-bottom: 5px;
|
|
|
color: #4a6fa5;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.grid-form input,
|
|
|
.grid-form textarea {
|
|
|
width: 100%;
|
|
|
padding: 8px 12px;
|
|
|
border: 1px solid #d1e0f5;
|
|
|
border-radius: 6px;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.grid-form textarea {
|
|
|
min-height: 80px;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
<div class="main-container">
|
|
|
<!-- 左侧内容区域 -->
|
|
|
<div class="content-area">
|
|
|
<div class="personnel-header">
|
|
|
<h1 class="personnel-title"><i class="fas fa-users"></i> 人员管理系统</h1>
|
|
|
<div class="personnel-actions">
|
|
|
<button class="action-btn" onclick="openPersonnelModal()"><i class="fas fa-user-plus"></i>
|
|
|
新增人员</button>
|
|
|
<button class="action-btn" onclick="openScheduleModal()"><i class="fas fa-calendar-alt"></i>
|
|
|
排班管理</button>
|
|
|
<button class="action-btn" onclick="exportReport()"><i class="fas fa-file-export"></i> 导出报表</button>
|
|
|
<button class="action-btn" onclick="location.href='index.html'"><i class="fas fa-home"
|
|
|
style="color: orange;"></i> 回到首页</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="personnel-tabs">
|
|
|
<button class="tab-btn active" onclick="switchTab('overview')">概览统计</button>
|
|
|
<button class="tab-btn" onclick="switchTab('personnel')">人员档案</button>
|
|
|
<button class="tab-btn" onclick="switchTab('grid')">网格分配</button>
|
|
|
<button class="tab-btn" onclick="switchTab('schedule')">排班日历</button>
|
|
|
<button class="tab-btn" onclick="switchTab('attendance')">考勤管理</button>
|
|
|
<button class="tab-btn" onclick="switchTab('performance')">绩效考核</button>
|
|
|
</div>
|
|
|
|
|
|
<!-- 概览统计 -->
|
|
|
<div id="overview" class="tab-content active">
|
|
|
<div class="personnel-card">
|
|
|
<div class="card-title">
|
|
|
<i class="fas fa-tachometer-alt"></i>
|
|
|
人员概览统计
|
|
|
</div>
|
|
|
<div class="stats-grid">
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value" id="totalPersonnel">28</span>
|
|
|
<div class="stat-label">总人数</div>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value" id="onDutyPersonnel">26</span>
|
|
|
<div class="stat-label">在岗人数</div>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value" id="gridCount">12</span>
|
|
|
<div class="stat-label">网格数量</div>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value" id="attendanceRate">92.8%</span>
|
|
|
<div class="stat-label">出勤率</div>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value" id="workDistance">156.8km</span>
|
|
|
<div class="stat-label">作业里程</div>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value" id="completionRate">94.2%</span>
|
|
|
<div class="stat-label">任务完成率</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="chart-container" style="height: 420px;;">
|
|
|
<canvas id="performanceChart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 人员档案 -->
|
|
|
<div id="personnel" class="tab-content">
|
|
|
<div class="personnel-card">
|
|
|
<div class="card-title">
|
|
|
<i class="fas fa-user-friends"></i>
|
|
|
人员档案管理
|
|
|
</div>
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>岗位类型</label>
|
|
|
<select id="positionFilter">
|
|
|
<option value="all">全部岗位</option>
|
|
|
<option value="cleaner">保洁员</option>
|
|
|
<option value="supervisor">班长</option>
|
|
|
<option value="manager">主管</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>所属网格</label>
|
|
|
<select id="gridFilter">
|
|
|
<option value="all">全部网格</option>
|
|
|
<option value="grid1">网格A1</option>
|
|
|
<option value="grid2">网格A2</option>
|
|
|
<option value="grid3">网格B1</option>
|
|
|
<option value="grid4">网格B2</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>在岗状态</label>
|
|
|
<select id="statusFilter">
|
|
|
<option value="all">全部状态</option>
|
|
|
<option value="online">在岗</option>
|
|
|
<option value="offline">离岗</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>搜索人员</label>
|
|
|
<input type="text" id="personnelSearch" placeholder="输入姓名或工号">
|
|
|
</div>
|
|
|
</div>
|
|
|
<table class="personnel-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>工号</th>
|
|
|
<th>姓名</th>
|
|
|
<th>岗位</th>
|
|
|
<th>所属网格</th>
|
|
|
<th>联系电话</th>
|
|
|
<th>在岗状态</th>
|
|
|
<th>本月考勤</th>
|
|
|
<th>绩效评分</th>
|
|
|
<th>操作</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody id="personnelTableBody">
|
|
|
<!-- 人员数据将通过JS动态加载 -->
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div id="grid" class="tab-content">
|
|
|
<div class="personnel-card">
|
|
|
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
<div>
|
|
|
<i class="fas fa-th"></i>
|
|
|
网格人员分配
|
|
|
</div>
|
|
|
<div class="grid-actions">
|
|
|
<button class="btn btn-primary" onclick="optimizeAllocation()">
|
|
|
<i class="fas fa-magic"></i> 智能分配
|
|
|
</button>
|
|
|
<button class="btn btn-success" onclick="manageGrids()">
|
|
|
<i class="fas fa-cogs"></i> 网格管理
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="personnel-grid" id="gridAllocationGrid">
|
|
|
<!-- 网格分配卡片将通过JS动态加载 -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<!-- 修改后的排班日历部分 -->
|
|
|
<div id="schedule" class="tab-content">
|
|
|
<div class="personnel-card">
|
|
|
<div class="card-title">
|
|
|
<i class="fas fa-calendar-alt"></i>
|
|
|
排班日历
|
|
|
</div>
|
|
|
<div class="schedule-calendar">
|
|
|
<div class="calendar-header">
|
|
|
<h3 id="currentMonthTitle">2024年1月</h3>
|
|
|
<div class="calendar-nav">
|
|
|
<button class="calendar-nav-btn" onclick="prevMonth()"><i
|
|
|
class="fas fa-chevron-left"></i> 上月</button>
|
|
|
<button class="calendar-nav-btn" onclick="nextMonth()">下月 <i
|
|
|
class="fas fa-chevron-right"></i></button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="calendar-grid">
|
|
|
<div class="calendar-day header">周日</div>
|
|
|
<div class="calendar-day header">周一</div>
|
|
|
<div class="calendar-day header">周二</div>
|
|
|
<div class="calendar-day header">周三</div>
|
|
|
<div class="calendar-day header">周四</div>
|
|
|
<div class="calendar-day header">周五</div>
|
|
|
<div class="calendar-day header">周六</div>
|
|
|
<!-- 日期将通过JS动态加载 -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 考勤管理 -->
|
|
|
<div id="attendance" class="tab-content">
|
|
|
<div class="personnel-card">
|
|
|
<div class="card-title">
|
|
|
<i class="fas fa-clock"></i>
|
|
|
考勤管理
|
|
|
</div>
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>日期范围</label>
|
|
|
<input type="date" id="attendanceDate" value="2024-01-17">
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>网格筛选</label>
|
|
|
<select id="attendanceGridFilter">
|
|
|
<option value="all">全部网格</option>
|
|
|
<option value="grid1">网格A1</option>
|
|
|
<option value="grid2">网格A2</option>
|
|
|
<option value="grid3">网格B1</option>
|
|
|
<option value="grid4">网格B2</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>考勤状态</label>
|
|
|
<select id="attendanceStatusFilter">
|
|
|
<option value="all">全部状态</option>
|
|
|
<option value="normal">正常</option>
|
|
|
<option value="late">迟到</option>
|
|
|
<option value="absent">缺勤</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
</div>
|
|
|
<table class="personnel-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>姓名</th>
|
|
|
<th>工号</th>
|
|
|
<th>网格</th>
|
|
|
<th>上班打卡</th>
|
|
|
<th>下班打卡</th>
|
|
|
<th>工作时长</th>
|
|
|
<th>作业里程</th>
|
|
|
<th>考勤状态</th>
|
|
|
<th>操作</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody id="attendanceTableBody">
|
|
|
<!-- 考勤数据将通过JS动态加载 -->
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 绩效考核 -->
|
|
|
<div id="performance" class="tab-content">
|
|
|
<div class="personnel-card">
|
|
|
<div class="card-title">
|
|
|
<i class="fas fa-chart-bar"></i>
|
|
|
绩效考核
|
|
|
</div>
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>考核周期</label>
|
|
|
<select id="performancePeriod">
|
|
|
<option value="month">本月</option>
|
|
|
<option value="quarter">本季度</option>
|
|
|
<option value="year">本年度</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>岗位类型</label>
|
|
|
<select id="performancePositionFilter">
|
|
|
<option value="all">全部岗位</option>
|
|
|
<option value="cleaner">保洁员</option>
|
|
|
<option value="supervisor">班长</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>网格筛选</label>
|
|
|
<select id="performanceGridFilter">
|
|
|
<option value="all">全部网格</option>
|
|
|
<option value="grid1">网格A1</option>
|
|
|
<option value="grid2">网格A2</option>
|
|
|
<option value="grid3">网格B1</option>
|
|
|
<option value="grid4">网格B2</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
</div>
|
|
|
<table class="personnel-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>排名</th>
|
|
|
<th>姓名</th>
|
|
|
<th>工号</th>
|
|
|
<th>出勤率</th>
|
|
|
<th>作业时长</th>
|
|
|
<th>作业里程</th>
|
|
|
<th>完成率</th>
|
|
|
<th>质量评分</th>
|
|
|
<th>综合评分</th>
|
|
|
<th>操作</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody id="performanceTableBody">
|
|
|
<!-- 绩效数据将通过JS动态加载 -->
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 右侧侧边栏 -->
|
|
|
<div class="sidebar-area">
|
|
|
<div class="sidebar-section">
|
|
|
<div class="sidebar-title">
|
|
|
<i class="fas fa-tachometer-alt"></i>
|
|
|
人员概览
|
|
|
</div>
|
|
|
<div class="stats-grid">
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value">28</span>
|
|
|
<div class="stat-label">总人数</div>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value">26</span>
|
|
|
<div class="stat-label">在岗人数</div>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value">1</span>
|
|
|
<div class="stat-label">迟到人数</div>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-value">1</span>
|
|
|
<div class="stat-label">缺勤人数</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="sidebar-section">
|
|
|
<div class="sidebar-title">
|
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
|
考勤异常
|
|
|
</div>
|
|
|
<div class="alert-list" id="attendanceAlerts">
|
|
|
<!-- 考勤异常列表 -->
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="sidebar-section">
|
|
|
<div class="sidebar-title">
|
|
|
<i class="fas fa-chart-pie"></i>
|
|
|
岗位分布
|
|
|
</div>
|
|
|
<div class="chart-container">
|
|
|
<canvas id="positionChart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="sidebar-section">
|
|
|
<div class="sidebar-title">
|
|
|
<i class="fas fa-calendar-check"></i>
|
|
|
排班提醒
|
|
|
</div>
|
|
|
<div class="alert-list" id="scheduleReminders">
|
|
|
<!-- 排班提醒列表 -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 新增人员模态框 -->
|
|
|
<div id="personnelModal" class="modal">
|
|
|
<div class="modal-content">
|
|
|
<span class="close" onclick="closePersonnelModal()">×</span>
|
|
|
<h2>新增人员档案</h2>
|
|
|
<form id="personnelForm">
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>工号</label>
|
|
|
<input type="text" name="personnelId" placeholder="如:CL001" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>姓名</label>
|
|
|
<input type="text" name="personnelName" placeholder="输入人员姓名" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>岗位</label>
|
|
|
<select name="position" required>
|
|
|
<option value="">请选择岗位</option>
|
|
|
<option value="cleaner">保洁员</option>
|
|
|
<option value="supervisor">班长</option>
|
|
|
<option value="manager">主管</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>所属网格</label>
|
|
|
<select name="grid" required>
|
|
|
<option value="">请选择网格</option>
|
|
|
<option value="grid1">网格A1</option>
|
|
|
<option value="grid2">网格A2</option>
|
|
|
<option value="grid3">网格B1</option>
|
|
|
<option value="grid4">网格B2</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>联系电话</label>
|
|
|
<input type="tel" name="phone" placeholder="输入联系电话" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>入职日期</label>
|
|
|
<input type="date" name="joinDate" required>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>工作职责</label>
|
|
|
<textarea name="responsibilities" rows="3" placeholder="输入工作职责描述"></textarea>
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button type="button" class="btn" onclick="closePersonnelModal()">取消</button>
|
|
|
<button type="submit" class="btn btn-primary">保存人员</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 排班管理模态框 -->
|
|
|
<div id="scheduleModal" class="modal">
|
|
|
<div class="modal-content">
|
|
|
<span class="close" onclick="closeScheduleModal()">×</span>
|
|
|
<h2>排班管理</h2>
|
|
|
<form id="scheduleForm">
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>选择日期</label>
|
|
|
<input type="date" name="scheduleDate" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>班次</label>
|
|
|
<select name="shift" required>
|
|
|
<option value="">请选择班次</option>
|
|
|
<option value="morning">早班 (08:00-17:00)</option>
|
|
|
<option value="afternoon">晚班 (14:00-23:00)</option>
|
|
|
<option value="night">夜班 (23:00-08:00)</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>所属网格</label>
|
|
|
<select name="scheduleGrid" required>
|
|
|
<option value="">请选择网格</option>
|
|
|
<option value="grid1">网格A1</option>
|
|
|
<option value="grid2">网格A2</option>
|
|
|
<option value="grid3">网格B1</option>
|
|
|
<option value="grid4">网格B2</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>选择人员</label>
|
|
|
<div
|
|
|
style="max-height: 200px; overflow-y: auto; border: 1px solid #d1e0f5; border-radius: 6px; padding: 10px;">
|
|
|
<!-- 人员选择列表将通过JS动态加载 -->
|
|
|
<div id="personnelChecklist"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button type="button" class="btn" onclick="closeScheduleModal()">取消</button>
|
|
|
<button type="submit" class="btn btn-primary">保存排班</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<!-- 新增网格管理模态框 -->
|
|
|
<div id="gridManageModal" class="modal">
|
|
|
<div class="modal-content grid-modal-content">
|
|
|
<span class="close" onclick="closeModal2('gridManageModal')">×</span>
|
|
|
<h2>网格管理</h2>
|
|
|
<div style="margin-bottom: 20px;">
|
|
|
<button class="btn btn-primary" onclick="showAddGridForm()">
|
|
|
<i class="fas fa-plus"></i> 新增网格
|
|
|
</button>
|
|
|
</div>
|
|
|
<table class="grid-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>网格ID</th>
|
|
|
<th>网格名称</th>
|
|
|
<th>覆盖面积</th>
|
|
|
<th>描述</th>
|
|
|
<th>操作</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody id="gridTableBody">
|
|
|
<!-- 网格数据将通过JS动态加载 -->
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 新增/编辑网格表单模态框 -->
|
|
|
<div id="gridFormModal" class="modal">
|
|
|
<div class="modal-content">
|
|
|
<span class="close" onclick="closeModal2('gridFormModal')">×</span>
|
|
|
<h2 id="gridFormTitle">新增网格</h2>
|
|
|
<form id="gridForm" class="grid-form">
|
|
|
<input type="hidden" id="gridId">
|
|
|
<div class="form-group">
|
|
|
<label for="gridName">网格名称</label>
|
|
|
<input type="text" id="gridName" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label for="gridArea">覆盖面积</label>
|
|
|
<input type="text" id="gridArea" placeholder="例如: 0.8km²" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label for="gridDescription">描述</label>
|
|
|
<textarea id="gridDescription" required></textarea>
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button type="button" class="btn" onclick="closeModal2('gridFormModal')">取消</button>
|
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
</div>
|
|
|
</div>
|
|
|
<script>
|
|
|
// 模拟数据
|
|
|
const personnelData = [
|
|
|
{ id: 'CL001', name: '张三', position: 'cleaner', grid: 'grid1', phone: '138****1234', status: 'online', attendance: '22/22', score: 96, lastUpdate: '2024-01-17 14:30' },
|
|
|
{ id: 'CL002', name: '李四', position: 'cleaner', grid: 'grid2', phone: '139****5678', status: 'offline', attendance: '21/22', score: 75, lastUpdate: '2024-01-17 12:15' },
|
|
|
{ id: 'SV001', name: '王五', position: 'supervisor', grid: 'grid1', phone: '137****9012', status: 'online', attendance: '22/22', score: 91, lastUpdate: '2024-01-17 14:35' },
|
|
|
{ id: 'CL006', name: '赵六', position: 'cleaner', grid: 'grid1', phone: '136****3456', status: 'online', attendance: '22/22', score: 88, lastUpdate: '2024-01-17 10:00' },
|
|
|
{ id: 'CL007', name: '钱七', position: 'cleaner', grid: 'grid3', phone: '135****7890', status: 'online', attendance: '21/22', score: 87, lastUpdate: '2024-01-17 09:30' }
|
|
|
];
|
|
|
|
|
|
const gridData = [
|
|
|
{ id: 'grid1', name: '网格A1', area: '0.8km²', personnel: ['CL001', 'SV001', 'CL006'], stats: { distance: '6.2km', completion: '95%' } },
|
|
|
{ id: 'grid2', name: '网格A2', area: '0.9km²', personnel: ['CL002'], stats: { distance: '3.1km', completion: '68%' } },
|
|
|
{ id: 'grid3', name: '网格B1', area: '1.2km²', personnel: ['CL007'], stats: { distance: '8.7km', completion: '92%' } },
|
|
|
{ id: 'grid4', name: '网格B2', area: '1.1km²', personnel: [], stats: { distance: '0km', completion: '0%' } }
|
|
|
];
|
|
|
|
|
|
const attendanceData = [
|
|
|
{ id: 'A001', personnelId: 'CL001', date: '2024-01-17', clockIn: '08:00', clockOut: '17:30', status: 'normal', distance: '6.2km', hours: '8.5小时' },
|
|
|
{ id: 'A002', personnelId: 'CL002', date: '2024-01-17', clockIn: '08:15', clockOut: '--', status: 'late', distance: '3.1km', hours: '--' },
|
|
|
{ id: 'A003', personnelId: 'SV001', date: '2024-01-17', clockIn: '07:45', clockOut: '18:00', status: 'normal', distance: '12.5km', hours: '9.2小时' },
|
|
|
{ id: 'A004', personnelId: 'CL006', date: '2024-01-17', clockIn: '--', clockOut: '--', status: 'absent', distance: '0km', hours: '0小时' },
|
|
|
{ id: 'A005', personnelId: 'CL007', date: '2024-01-17', clockIn: '08:05', clockOut: '17:45', status: 'normal', distance: '8.7km', hours: '8.7小时' }
|
|
|
];
|
|
|
|
|
|
const performanceData = [
|
|
|
{ id: 'P001', personnelId: 'CL001', period: '2024-01', attendanceRate: '100%', workHours: '187小时', workDistance: '136.4km', completionRate: '98%', qualityScore: '95分', totalScore: '96分' },
|
|
|
{ id: 'P002', personnelId: 'SV001', period: '2024-01', attendanceRate: '100%', workHours: '202小时', workDistance: '275km', completionRate: '96%', qualityScore: '92分', totalScore: '91分' },
|
|
|
{ id: 'P003', personnelId: 'CL007', period: '2024-01', attendanceRate: '95%', workHours: '178小时', workDistance: '191.3km', completionRate: '94%', qualityScore: '88分', totalScore: '87分' },
|
|
|
{ id: 'P004', personnelId: 'CL002', period: '2024-01', attendanceRate: '95%', workHours: '165小时', workDistance: '68.2km', completionRate: '85%', qualityScore: '82分', totalScore: '75分' },
|
|
|
{ id: 'P005', personnelId: 'CL006', period: '2024-01', attendanceRate: '100%', workHours: '180小时', workDistance: '120.5km', completionRate: '90%', qualityScore: '85分', totalScore: '88分' }
|
|
|
];
|
|
|
|
|
|
// 标签页切换
|
|
|
function switchTab(tabName) {
|
|
|
// 隐藏所有标签页内容
|
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
|
content.classList.remove('active');
|
|
|
});
|
|
|
|
|
|
// 移除所有标签按钮的激活状态
|
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
|
btn.classList.remove('active');
|
|
|
});
|
|
|
|
|
|
// 显示选中的标签页内容
|
|
|
document.getElementById(tabName).classList.add('active');
|
|
|
|
|
|
// 激活选中的标签按钮
|
|
|
event.target.classList.add('active');
|
|
|
|
|
|
// 根据标签页加载对应数据
|
|
|
switch (tabName) {
|
|
|
case 'overview':
|
|
|
loadOverviewData();
|
|
|
break;
|
|
|
case 'personnel':
|
|
|
loadPersonnelData();
|
|
|
break;
|
|
|
case 'grid':
|
|
|
loadGridData();
|
|
|
break;
|
|
|
case 'attendance':
|
|
|
loadAttendanceData();
|
|
|
break;
|
|
|
case 'performance':
|
|
|
loadPerformanceData();
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 加载概览数据
|
|
|
function loadOverviewData() {
|
|
|
// 更新统计数据
|
|
|
document.getElementById('totalPersonnel').textContent = personnelData.length;
|
|
|
document.getElementById('onDutyPersonnel').textContent = personnelData.filter(p => p.status === 'online').length;
|
|
|
document.getElementById('gridCount').textContent = gridData.length;
|
|
|
|
|
|
// 计算平均出勤率
|
|
|
const totalPresent = personnelData.reduce((sum, p) => {
|
|
|
const [present] = p.attendance.split('/').map(Number);
|
|
|
return sum + present;
|
|
|
}, 0);
|
|
|
const totalDays = personnelData.reduce((sum, p) => {
|
|
|
const [, total] = p.attendance.split('/').map(Number);
|
|
|
return sum + total;
|
|
|
}, 0);
|
|
|
const avgAttendance = (totalPresent / totalDays * 100).toFixed(1);
|
|
|
document.getElementById('attendanceRate').textContent = `${avgAttendance}%`;
|
|
|
|
|
|
// 计算平均工作距离
|
|
|
const totalDistance = attendanceData.reduce((sum, a) => {
|
|
|
return sum + (parseFloat(a.distance) || 0);
|
|
|
}, 0);
|
|
|
const avgDistance = (totalDistance / attendanceData.length).toFixed(1);
|
|
|
document.getElementById('workDistance').textContent = `${avgDistance}km`;
|
|
|
|
|
|
// 计算平均完成率
|
|
|
const totalCompletion = gridData.reduce((sum, g) => {
|
|
|
return sum + (parseFloat(g.stats.completion) || 0);
|
|
|
}, 0);
|
|
|
const avgCompletion = (totalCompletion / gridData.length).toFixed(1);
|
|
|
document.getElementById('completionRate').textContent = `${avgCompletion}%`;
|
|
|
|
|
|
// 绘制图表
|
|
|
drawPerformanceChart();
|
|
|
}
|
|
|
|
|
|
// 加载人员数据
|
|
|
function loadPersonnelData() {
|
|
|
const personnelTableBody = document.getElementById('personnelTableBody');
|
|
|
if (!personnelTableBody) return;
|
|
|
|
|
|
const statusMap = {
|
|
|
'online': { text: '在岗', class: 'status-online' },
|
|
|
'offline': { text: '离岗', class: 'status-offline' }
|
|
|
};
|
|
|
|
|
|
const positionMap = {
|
|
|
'cleaner': '保洁员',
|
|
|
'supervisor': '班长',
|
|
|
'manager': '主管'
|
|
|
};
|
|
|
|
|
|
personnelTableBody.innerHTML = personnelData.map(person => {
|
|
|
const grid = gridData.find(g => g.id === person.grid);
|
|
|
return `
|
|
|
<tr>
|
|
|
<td>${person.id}</td>
|
|
|
<td>${person.name}</td>
|
|
|
<td>${positionMap[person.position]}</td>
|
|
|
<td>${grid ? grid.name : '未分配'}</td>
|
|
|
<td>${person.phone}</td>
|
|
|
<td><span class="status-badge ${statusMap[person.status].class}">${statusMap[person.status].text}</span></td>
|
|
|
<td>${person.attendance}</td>
|
|
|
<td>${person.score}分</td>
|
|
|
<td>
|
|
|
<button class="btn btn-primary" onclick="viewPersonnelDetails('${person.id}')">查看</button>
|
|
|
<button class="btn btn-success" onclick="editPersonnel('${person.id}')">编辑</button>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
|
|
|
// 设置筛选器事件监听
|
|
|
setupFilterEvents();
|
|
|
}
|
|
|
|
|
|
// 设置筛选事件
|
|
|
function setupFilterEvents() {
|
|
|
document.getElementById('positionFilter').addEventListener('change', filterPersonnel);
|
|
|
document.getElementById('gridFilter').addEventListener('change', filterPersonnel);
|
|
|
document.getElementById('statusFilter').addEventListener('change', filterPersonnel);
|
|
|
document.getElementById('personnelSearch').addEventListener('input', filterPersonnel);
|
|
|
}
|
|
|
|
|
|
// 人员筛选功能
|
|
|
function filterPersonnel() {
|
|
|
const positionFilter = document.getElementById('positionFilter').value;
|
|
|
const gridFilter = document.getElementById('gridFilter').value;
|
|
|
const statusFilter = document.getElementById('statusFilter').value;
|
|
|
const searchText = document.getElementById('personnelSearch').value.toLowerCase();
|
|
|
|
|
|
const rows = document.querySelectorAll('#personnelTableBody tr');
|
|
|
|
|
|
rows.forEach(row => {
|
|
|
const name = row.cells[1].textContent.toLowerCase();
|
|
|
const id = row.cells[0].textContent.toLowerCase();
|
|
|
const position = row.cells[2].textContent;
|
|
|
const grid = row.cells[3].textContent;
|
|
|
const status = row.cells[5].textContent;
|
|
|
|
|
|
const matchesSearch = name.includes(searchText) || id.includes(searchText);
|
|
|
const matchesPosition = positionFilter === 'all' || position.includes(getPositionText(positionFilter));
|
|
|
const matchesGrid = gridFilter === 'all' || grid.includes(getGridText(gridFilter));
|
|
|
const matchesStatus = statusFilter === 'all' || status.includes(getStatusText(statusFilter));
|
|
|
|
|
|
row.style.display = matchesSearch && matchesPosition && matchesGrid && matchesStatus ? '' : 'none';
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function getPositionText(value) {
|
|
|
const positions = { 'cleaner': '保洁员', 'supervisor': '班长', 'manager': '主管' };
|
|
|
return positions[value] || value;
|
|
|
}
|
|
|
|
|
|
function getGridText(value) {
|
|
|
const grid = gridData.find(g => g.id === value);
|
|
|
return grid ? grid.name : value;
|
|
|
}
|
|
|
|
|
|
function getStatusText(value) {
|
|
|
const statuses = { 'online': '在岗', 'offline': '离岗' };
|
|
|
return statuses[value] || value;
|
|
|
}
|
|
|
|
|
|
// 加载网格数据
|
|
|
function loadGridData() {
|
|
|
const gridAllocationGrid = document.getElementById('gridAllocationGrid');
|
|
|
if (!gridAllocationGrid) return;
|
|
|
|
|
|
gridAllocationGrid.innerHTML = gridData.map(grid => {
|
|
|
const personnelList = grid.personnel.map(id => {
|
|
|
const person = personnelData.find(p => p.id === id);
|
|
|
if (!person) return '';
|
|
|
|
|
|
return `
|
|
|
<div class="personnel-info">
|
|
|
<div class="personnel-icon"><i class="fas fa-user"></i></div>
|
|
|
<div class="personnel-details">
|
|
|
<h4>${person.name}</h4>
|
|
|
<p>${person.id} - ${getPositionText(person.position)}</p>
|
|
|
</div>
|
|
|
<span class="status-badge ${person.status === 'online' ? 'status-online' : 'status-offline'}">
|
|
|
${person.status === 'online' ? '在岗' : '离岗'}
|
|
|
</span>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
|
|
|
return `
|
|
|
<div class="personnel-item">
|
|
|
<div class="card-title">
|
|
|
<i class="fas fa-th"></i>
|
|
|
${grid.name} (${grid.area})
|
|
|
</div>
|
|
|
${personnelList || '<p style="color: #4a6fa5; text-align: center;">暂无人员分配</p>'}
|
|
|
<div class="personnel-params">
|
|
|
<div class="param-item">
|
|
|
<span class="param-value">${grid.stats.distance}</span>
|
|
|
<div class="param-label">今日作业</div>
|
|
|
</div>
|
|
|
<div class="param-item">
|
|
|
<span class="param-value">${grid.stats.completion}</span>
|
|
|
<div class="param-label">完成率</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="margin-top: 15px;">
|
|
|
<button class="btn btn-primary" onclick="manageGrid('${grid.id}')"><i class="fas fa-cogs"></i> 网格管理</button>
|
|
|
<button class="btn btn-success" onclick="assignPersonnel('${grid.id}')"><i class="fas fa-hand-pointer"></i> 手动调整</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
// 更新日历相关函数
|
|
|
const scheduleData = {
|
|
|
currentDate: new Date()
|
|
|
};
|
|
|
|
|
|
// 加载月历
|
|
|
function loadMonthCalendar() {
|
|
|
const currentMonthTitle = document.getElementById('currentMonthTitle');
|
|
|
if (!currentMonthTitle) return;
|
|
|
|
|
|
const year = scheduleData.currentDate.getFullYear();
|
|
|
const month = scheduleData.currentDate.getMonth();
|
|
|
|
|
|
// 更新月份标题
|
|
|
currentMonthTitle.textContent = `${year}年${month + 1}月`;
|
|
|
|
|
|
// 获取当月第一天和最后一天
|
|
|
const firstDay = new Date(year, month, 1);
|
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
|
|
|
|
// 获取第一天是星期几 (0是周日)
|
|
|
const firstDayOfWeek = firstDay.getDay();
|
|
|
|
|
|
// 获取上个月的最后几天
|
|
|
const prevMonthLastDay = new Date(year, month, 0).getDate();
|
|
|
|
|
|
// 生成日历网格
|
|
|
const calendarGrid = document.querySelector('#schedule .calendar-grid');
|
|
|
if (!calendarGrid) return;
|
|
|
|
|
|
// 清空除表头外的所有日期单元格
|
|
|
while (calendarGrid.children.length > 7) {
|
|
|
calendarGrid.removeChild(calendarGrid.lastChild);
|
|
|
}
|
|
|
|
|
|
// 添加上个月的日期
|
|
|
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
|
|
|
const day = prevMonthLastDay - i;
|
|
|
addDayCell(calendarGrid, new Date(year, month - 1, day), true);
|
|
|
}
|
|
|
|
|
|
// 添加当月的日期
|
|
|
for (let day = 1; day <= lastDay.getDate(); day++) {
|
|
|
const date = new Date(year, month, day);
|
|
|
addDayCell(calendarGrid, date, false);
|
|
|
}
|
|
|
|
|
|
// 添加下个月的日期 (补齐最后一行)
|
|
|
const totalCells = Math.ceil((firstDayOfWeek + lastDay.getDate()) / 7) * 7;
|
|
|
const nextMonthDays = totalCells - (firstDayOfWeek + lastDay.getDate());
|
|
|
|
|
|
for (let day = 1; day <= nextMonthDays; day++) {
|
|
|
addDayCell(calendarGrid, new Date(year, month + 1, day), true);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 添加日期单元格
|
|
|
function addDayCell(calendarGrid, date, isOtherMonth) {
|
|
|
const dayNumber = date.getDate();
|
|
|
const dayKey = formatDate(date);
|
|
|
const today = new Date();
|
|
|
|
|
|
// 检查是否是今天
|
|
|
const isToday = date.getDate() === today.getDate() &&
|
|
|
date.getMonth() === today.getMonth() &&
|
|
|
date.getFullYear() === today.getFullYear();
|
|
|
|
|
|
// 检查是否有排班
|
|
|
const hasSchedule = scheduleData.schedules &&
|
|
|
scheduleData.schedules[dayKey] &&
|
|
|
scheduleData.schedules[dayKey].length > 0;
|
|
|
|
|
|
// 创建日期单元格
|
|
|
const dayCell = document.createElement('div');
|
|
|
dayCell.className = 'calendar-day';
|
|
|
if (isToday) dayCell.classList.add('today');
|
|
|
if (hasSchedule) dayCell.classList.add('scheduled');
|
|
|
if (isOtherMonth) dayCell.classList.add('other-month');
|
|
|
|
|
|
// 添加日期数字
|
|
|
const dayNumberDiv = document.createElement('div');
|
|
|
dayNumberDiv.className = 'day-number';
|
|
|
dayNumberDiv.textContent = dayNumber;
|
|
|
dayCell.appendChild(dayNumberDiv);
|
|
|
|
|
|
// 添加人员列表
|
|
|
const dayStaffDiv = document.createElement('div');
|
|
|
dayStaffDiv.className = 'day-staff';
|
|
|
|
|
|
if (hasSchedule) {
|
|
|
scheduleData.schedules[dayKey].forEach(personId => {
|
|
|
const person = personnelData.find(p => p.id === personId);
|
|
|
if (person) {
|
|
|
const staffItem = document.createElement('div');
|
|
|
staffItem.className = 'day-staff-item';
|
|
|
staffItem.innerHTML = `<i class="fas fa-user"></i>${person.name} (${person.id})`;
|
|
|
dayStaffDiv.appendChild(staffItem);
|
|
|
}
|
|
|
});
|
|
|
} else if (!isOtherMonth) {
|
|
|
dayStaffDiv.textContent = '待排班';
|
|
|
}
|
|
|
|
|
|
dayCell.appendChild(dayStaffDiv);
|
|
|
calendarGrid.appendChild(dayCell);
|
|
|
}
|
|
|
|
|
|
// 上个月
|
|
|
function prevMonth() {
|
|
|
scheduleData.currentDate.setMonth(scheduleData.currentDate.getMonth() - 1);
|
|
|
loadMonthCalendar();
|
|
|
}
|
|
|
|
|
|
// 下个月
|
|
|
function nextMonth() {
|
|
|
scheduleData.currentDate.setMonth(scheduleData.currentDate.getMonth() + 1);
|
|
|
loadMonthCalendar();
|
|
|
}
|
|
|
|
|
|
// 初始化日历数据
|
|
|
scheduleData.schedules = {
|
|
|
'2024-01-15': ['CL001', 'CL002'],
|
|
|
'2024-01-16': ['CL001', 'SV001'],
|
|
|
'2024-01-17': ['CL002', 'SV001'],
|
|
|
'2024-01-22': ['CL001', 'CL007'],
|
|
|
'2024-01-23': ['CL002', 'SV001'],
|
|
|
'2024-01-24': ['CL006', 'SV001']
|
|
|
};
|
|
|
|
|
|
// 页面加载完成后初始化日历
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
loadMonthCalendar();
|
|
|
});
|
|
|
|
|
|
// 格式化日期为YYYY-MM-DD
|
|
|
function formatDate(date) {
|
|
|
const year = date.getFullYear();
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
|
return `${year}-${month}-${day}`;
|
|
|
}
|
|
|
|
|
|
// 加载考勤数据
|
|
|
function loadAttendanceData() {
|
|
|
const attendanceTableBody = document.getElementById('attendanceTableBody');
|
|
|
if (!attendanceTableBody) return;
|
|
|
|
|
|
const statusMap = {
|
|
|
'normal': { text: '正常', class: 'status-normal' },
|
|
|
'late': { text: '迟到', class: 'status-late' },
|
|
|
'absent': { text: '缺勤', class: 'status-absent' }
|
|
|
};
|
|
|
|
|
|
attendanceTableBody.innerHTML = attendanceData.map(record => {
|
|
|
const person = personnelData.find(p => p.id === record.personnelId);
|
|
|
if (!person) return '';
|
|
|
|
|
|
return `
|
|
|
<tr>
|
|
|
<td>${person.name}</td>
|
|
|
<td>${record.personnelId}</td>
|
|
|
<td>${getGridText(person.grid)}</td>
|
|
|
<td>${record.clockIn}</td>
|
|
|
<td>${record.clockOut}</td>
|
|
|
<td>${record.hours}</td>
|
|
|
<td>${record.distance}</td>
|
|
|
<td><span class="status-badge ${statusMap[record.status].class}">${statusMap[record.status].text}</span></td>
|
|
|
<td>
|
|
|
<button class="btn btn-primary" onclick="viewAttendanceDetails('${record.id}')">详情</button>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
|
|
|
// 设置筛选器事件监听
|
|
|
setupAttendanceFilterEvents();
|
|
|
}
|
|
|
|
|
|
// 设置考勤筛选事件
|
|
|
function setupAttendanceFilterEvents() {
|
|
|
document.getElementById('attendanceDate').addEventListener('change', filterAttendance);
|
|
|
document.getElementById('attendanceGridFilter').addEventListener('change', filterAttendance);
|
|
|
document.getElementById('attendanceStatusFilter').addEventListener('change', filterAttendance);
|
|
|
}
|
|
|
|
|
|
// 考勤筛选功能
|
|
|
function filterAttendance() {
|
|
|
const dateFilter = document.getElementById('attendanceDate').value;
|
|
|
const gridFilter = document.getElementById('attendanceGridFilter').value;
|
|
|
const statusFilter = document.getElementById('attendanceStatusFilter').value;
|
|
|
|
|
|
const rows = document.querySelectorAll('#attendanceTableBody tr');
|
|
|
|
|
|
rows.forEach(row => {
|
|
|
const grid = row.cells[2].textContent;
|
|
|
const status = row.cells[7].textContent;
|
|
|
|
|
|
const matchesGrid = gridFilter === 'all' || grid.includes(getGridText(gridFilter));
|
|
|
const matchesStatus = statusFilter === 'all' || status.includes(getAttendanceStatusText(statusFilter));
|
|
|
|
|
|
row.style.display = matchesGrid && matchesStatus ? '' : 'none';
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function getAttendanceStatusText(value) {
|
|
|
const statuses = { 'normal': '正常', 'late': '迟到', 'absent': '缺勤' };
|
|
|
return statuses[value] || value;
|
|
|
}
|
|
|
|
|
|
// 加载绩效数据
|
|
|
function loadPerformanceData() {
|
|
|
const performanceTableBody = document.getElementById('performanceTableBody');
|
|
|
if (!performanceTableBody) return;
|
|
|
|
|
|
performanceTableBody.innerHTML = performanceData.map(record => {
|
|
|
const person = personnelData.find(p => p.id === record.personnelId);
|
|
|
if (!person) return '';
|
|
|
|
|
|
return `
|
|
|
<tr>
|
|
|
<td>${record.id.replace('P', '')}</td>
|
|
|
<td>${person.name}</td>
|
|
|
<td>${record.personnelId}</td>
|
|
|
<td>${record.attendanceRate}</td>
|
|
|
<td>${record.workHours}</td>
|
|
|
<td>${record.workDistance}</td>
|
|
|
<td>${record.completionRate}</td>
|
|
|
<td>${record.qualityScore}</td>
|
|
|
<td><strong style="color: ${getScoreColor(record.totalScore)};">${record.totalScore}</strong></td>
|
|
|
<td>
|
|
|
<button class="btn btn-primary" onclick="viewPerformanceDetails('${record.id}')">详情</button>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
|
|
|
// 设置筛选器事件监听
|
|
|
setupPerformanceFilterEvents();
|
|
|
}
|
|
|
|
|
|
// 设置绩效筛选事件
|
|
|
function setupPerformanceFilterEvents() {
|
|
|
document.getElementById('performancePeriod').addEventListener('change', filterPerformance);
|
|
|
document.getElementById('performancePositionFilter').addEventListener('change', filterPerformance);
|
|
|
document.getElementById('performanceGridFilter').addEventListener('change', filterPerformance);
|
|
|
}
|
|
|
|
|
|
// 绩效筛选功能
|
|
|
function filterPerformance() {
|
|
|
const periodFilter = document.getElementById('performancePeriod').value;
|
|
|
const positionFilter = document.getElementById('performancePositionFilter').value;
|
|
|
const gridFilter = document.getElementById('performanceGridFilter').value;
|
|
|
|
|
|
const rows = document.querySelectorAll('#performanceTableBody tr');
|
|
|
|
|
|
rows.forEach(row => {
|
|
|
const id = row.cells[2].textContent;
|
|
|
const person = personnelData.find(p => p.id === id);
|
|
|
|
|
|
const matchesPosition = positionFilter === 'all' ||
|
|
|
(positionFilter === 'cleaner' && person.position === 'cleaner') ||
|
|
|
(positionFilter === 'supervisor' && person.position === 'supervisor');
|
|
|
const matchesGrid = gridFilter === 'all' ||
|
|
|
(person && person.grid === gridFilter);
|
|
|
|
|
|
row.style.display = matchesPosition && matchesGrid ? '' : 'none';
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function getScoreColor(score) {
|
|
|
const num = parseInt(score);
|
|
|
if (num >= 90) return '#28a745';
|
|
|
if (num >= 80) return '#ffc107';
|
|
|
return '#dc3545';
|
|
|
}
|
|
|
|
|
|
// 绘制绩效图表
|
|
|
function drawPerformanceChart() {
|
|
|
const ctx = document.getElementById('performanceChart').getContext('2d');
|
|
|
if (!ctx) return;
|
|
|
|
|
|
// 生成1-12月的随机绩效数据(80-100之间)
|
|
|
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
|
|
|
const scores = months.map(() => Math.floor(80 + Math.random() * 20));
|
|
|
|
|
|
new Chart(ctx, {
|
|
|
type: 'line',
|
|
|
data: {
|
|
|
labels: months,
|
|
|
datasets: [{
|
|
|
label: '绩效分数',
|
|
|
data: scores,
|
|
|
borderColor: '#2a7de1',
|
|
|
backgroundColor: 'rgba(42, 125, 225, 0.1)',
|
|
|
tension: 0.4,
|
|
|
borderWidth: 2,
|
|
|
fill: true
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
labels: {
|
|
|
color: '#1a4b8c',
|
|
|
font: {
|
|
|
weight: 'bold'
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
scales: {
|
|
|
x: {
|
|
|
ticks: { color: '#4a6fa5' },
|
|
|
grid: { color: 'rgba(209, 224, 245, 0.5)' }
|
|
|
},
|
|
|
y: {
|
|
|
min: 70,
|
|
|
max: 100,
|
|
|
ticks: { color: '#4a6fa5' },
|
|
|
grid: { color: 'rgba(209, 224, 245, 0.5)' }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 绘制岗位分布图表
|
|
|
function drawPositionChart() {
|
|
|
const ctx = document.getElementById('positionChart').getContext('2d');
|
|
|
if (!ctx) return;
|
|
|
|
|
|
// 统计各岗位人数
|
|
|
const positionCounts = {
|
|
|
'cleaner': personnelData.filter(p => p.position === 'cleaner').length,
|
|
|
'supervisor': personnelData.filter(p => p.position === 'supervisor').length,
|
|
|
'manager': personnelData.filter(p => p.position === 'manager').length
|
|
|
};
|
|
|
|
|
|
new Chart(ctx, {
|
|
|
type: 'doughnut',
|
|
|
data: {
|
|
|
labels: ['保洁员', '班长', '主管'],
|
|
|
datasets: [{
|
|
|
data: [positionCounts.cleaner, positionCounts.supervisor, positionCounts.manager],
|
|
|
backgroundColor: ['#2a7de1', '#28a745', '#ffc107'],
|
|
|
borderWidth: 1
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
labels: {
|
|
|
color: '#1a4b8c',
|
|
|
font: {
|
|
|
weight: 'bold'
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 模态框操作
|
|
|
function openPersonnelModal() {
|
|
|
document.getElementById('personnelModal').style.display = 'block';
|
|
|
}
|
|
|
|
|
|
function closePersonnelModal() {
|
|
|
document.getElementById('personnelModal').style.display = 'none';
|
|
|
}
|
|
|
|
|
|
function openScheduleModal() {
|
|
|
document.getElementById('scheduleModal').style.display = 'block';
|
|
|
loadPersonnelChecklist();
|
|
|
}
|
|
|
|
|
|
function closeScheduleModal() {
|
|
|
document.getElementById('scheduleModal').style.display = 'none';
|
|
|
}
|
|
|
|
|
|
// 加载人员选择列表
|
|
|
function loadPersonnelChecklist() {
|
|
|
const personnelChecklist = document.getElementById('personnelChecklist');
|
|
|
if (!personnelChecklist) return;
|
|
|
|
|
|
personnelChecklist.innerHTML = personnelData.map(person => `
|
|
|
<div style="margin-bottom: 10px; padding: 8px; background: #f5f9ff; border-radius: 6px;">
|
|
|
<label style="display: flex; align-items: center; gap: 10px;">
|
|
|
<input type="checkbox" name="selectedPersonnel" value="${person.id}">
|
|
|
<div style="flex: 1;">
|
|
|
<div style="font-weight: 500; color: #1a4b8c;">${person.name} (${person.id})</div>
|
|
|
<div style="font-size: 12px; color: #4a6fa5;">
|
|
|
${getPositionText(person.position)} | ${getGridText(person.grid)} |
|
|
|
<span class="status-badge ${person.status === 'online' ? 'status-online' : 'status-offline'}"
|
|
|
style="font-size: 11px; padding: 2px 6px;">
|
|
|
${person.status === 'online' ? '在岗' : '离岗'}
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</label>
|
|
|
</div>
|
|
|
`).join('');
|
|
|
}
|
|
|
|
|
|
// 人员操作函数
|
|
|
function viewPersonnelDetails(id) {
|
|
|
const person = personnelData.find(p => p.id === id);
|
|
|
if (!person) return;
|
|
|
|
|
|
const modal = createDetailsModal(
|
|
|
`${person.name} (${person.id})`,
|
|
|
`
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>岗位</label>
|
|
|
<div>${getPositionText(person.position)}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>所属网格</label>
|
|
|
<div>${getGridText(person.grid)}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>联系电话</label>
|
|
|
<div>${person.phone}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>在岗状态</label>
|
|
|
<div><span class="status-badge ${person.status === 'online' ? 'status-online' : 'status-offline'}">
|
|
|
${person.status === 'online' ? '在岗' : '离岗'}
|
|
|
</span></div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>本月考勤</label>
|
|
|
<div>${person.attendance}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>绩效评分</label>
|
|
|
<div><strong style="color: ${getScoreColor(person.score)};">${person.score}分</strong></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>最后更新</label>
|
|
|
<div>${person.lastUpdate}</div>
|
|
|
</div>
|
|
|
`
|
|
|
);
|
|
|
document.body.appendChild(modal);
|
|
|
}
|
|
|
|
|
|
function editPersonnel(id) {
|
|
|
const person = personnelData.find(p => p.id === id);
|
|
|
if (!person) return;
|
|
|
|
|
|
const modal = createEditModal(
|
|
|
`编辑人员 - ${person.name} (${person.id})`,
|
|
|
`
|
|
|
<form id="editPersonnelForm">
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>姓名</label>
|
|
|
<input type="text" name="name" value="${person.name}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>岗位</label>
|
|
|
<select name="position" required>
|
|
|
<option value="cleaner" ${person.position === 'cleaner' ? 'selected' : ''}>保洁员</option>
|
|
|
<option value="supervisor" ${person.position === 'supervisor' ? 'selected' : ''}>班长</option>
|
|
|
<option value="manager" ${person.position === 'manager' ? 'selected' : ''}>主管</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>所属网格</label>
|
|
|
<select name="grid" required>
|
|
|
${gridData.map(grid => `
|
|
|
<option value="${grid.id}" ${person.grid === grid.id ? 'selected' : ''}>
|
|
|
${grid.name}
|
|
|
</option>
|
|
|
`).join('')}
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>联系电话</label>
|
|
|
<input type="tel" name="phone" value="${person.phone}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>在岗状态</label>
|
|
|
<select name="status" required>
|
|
|
<option value="online" ${person.status === 'online' ? 'selected' : ''}>在岗</option>
|
|
|
<option value="offline" ${person.status === 'offline' ? 'selected' : ''}>离岗</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button type="button" class="btn" onclick="closeModal(this)">取消</button>
|
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
`
|
|
|
);
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
// 表单提交处理
|
|
|
document.getElementById('editPersonnelForm').addEventListener('submit', function (e) {
|
|
|
e.preventDefault();
|
|
|
alert('人员信息已更新');
|
|
|
closeModal(document.querySelector('.modal-overlay'));
|
|
|
loadPersonnelData();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 网格操作函数
|
|
|
function manageGrid(id) {
|
|
|
const grid = gridData.find(g => g.id === id);
|
|
|
if (!grid) return;
|
|
|
|
|
|
const modal = createDetailsModal(
|
|
|
`管理网格 - ${grid.name}`,
|
|
|
`
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>网格ID</label>
|
|
|
<div>${grid.id}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>覆盖面积</label>
|
|
|
<div>${grid.area}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>分配人员数</label>
|
|
|
<div>${grid.personnel.length}人</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>今日作业</label>
|
|
|
<div>${grid.stats.distance}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>完成率</label>
|
|
|
<div>${grid.stats.completion}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>分配人员</label>
|
|
|
<div style="max-height: 150px; overflow-y: auto; border: 1px solid #e1e9f5; border-radius: 6px; padding: 10px;">
|
|
|
${grid.personnel.length > 0 ?
|
|
|
grid.personnel.map(id => {
|
|
|
const person = personnelData.find(p => p.id === id);
|
|
|
return person ? `
|
|
|
<div style="padding: 8px; border-bottom: 1px solid #e1e9f5; display: flex; justify-content: space-between; align-items: center;">
|
|
|
<span>${person.name} (${person.id})</span>
|
|
|
<button class="btn btn-danger" style="padding: 2px 8px; font-size: 12px;"
|
|
|
onclick="removeFromGrid('${grid.id}', '${person.id}')">
|
|
|
移除
|
|
|
</button>
|
|
|
</div>
|
|
|
` : '';
|
|
|
}).join('') :
|
|
|
'<div style="text-align: center; color: #4a6fa5; padding: 10px;">暂无分配人员</div>'
|
|
|
}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button class="btn btn-primary" onclick="assignPersonnel('${grid.id}')">分配人员</button>
|
|
|
</div>
|
|
|
`
|
|
|
);
|
|
|
document.body.appendChild(modal);
|
|
|
}
|
|
|
|
|
|
function assignPersonnel(gridId) {
|
|
|
const grid = gridData.find(g => g.id === gridId);
|
|
|
if (!grid) return;
|
|
|
|
|
|
const modal = createEditModal(
|
|
|
`为 ${grid.name} 分配人员`,
|
|
|
`
|
|
|
<div style="max-height: 400px; overflow-y: auto;">
|
|
|
${personnelData.map(person => `
|
|
|
<div style="margin-bottom: 10px; padding: 10px; background: #f5f9ff; border-radius: 6px;
|
|
|
display: flex; align-items: center; justify-content: space-between;">
|
|
|
<div>
|
|
|
<div style="font-weight: 500; color: #1a4b8c;">${person.name} (${person.id})</div>
|
|
|
<div style="font-size: 12px; color: #4a6fa5;">
|
|
|
${getPositionText(person.position)} | ${getGridText(person.grid)}
|
|
|
</div>
|
|
|
</div>
|
|
|
<label style="display: flex; align-items: center; gap: 8px;">
|
|
|
<input type="checkbox" ${grid.personnel.includes(person.id) ? 'checked' : ''}
|
|
|
onchange="toggleGridAssignment('${gridId}', '${person.id}', this.checked)">
|
|
|
<span style="font-size: 12px;">分配</span>
|
|
|
</label>
|
|
|
</div>
|
|
|
`).join('')}
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button type="button" class="btn" onclick="closeModal(this)">完成</button>
|
|
|
</div>
|
|
|
`
|
|
|
);
|
|
|
document.body.appendChild(modal);
|
|
|
}
|
|
|
|
|
|
function toggleGridAssignment(gridId, personnelId, assign) {
|
|
|
const grid = gridData.find(g => g.id === gridId);
|
|
|
if (!grid) return;
|
|
|
|
|
|
if (assign) {
|
|
|
if (!grid.personnel.includes(personnelId)) {
|
|
|
grid.personnel.push(personnelId);
|
|
|
}
|
|
|
} else {
|
|
|
grid.personnel = grid.personnel.filter(id => id !== personnelId);
|
|
|
}
|
|
|
|
|
|
// 更新网格数据
|
|
|
loadGridData();
|
|
|
}
|
|
|
|
|
|
function removeFromGrid(gridId, personnelId) {
|
|
|
const grid = gridData.find(g => g.id === gridId);
|
|
|
if (!grid) return;
|
|
|
|
|
|
grid.personnel = grid.personnel.filter(id => id !== personnelId);
|
|
|
|
|
|
// 关闭当前模态框并重新打开管理界面
|
|
|
closeModal(document.querySelector('.modal-overlay'));
|
|
|
manageGrid(gridId);
|
|
|
}
|
|
|
|
|
|
// 考勤操作函数
|
|
|
function viewAttendanceDetails(id) {
|
|
|
const record = attendanceData.find(a => a.id === id);
|
|
|
if (!record) return;
|
|
|
|
|
|
const person = personnelData.find(p => p.id === record.personnelId);
|
|
|
if (!person) return;
|
|
|
|
|
|
const modal = createDetailsModal(
|
|
|
`考勤详情 - ${person.name} (${record.personnelId})`,
|
|
|
`
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>日期</label>
|
|
|
<div>${record.date}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>上班打卡</label>
|
|
|
<div>${record.clockIn}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>下班打卡</label>
|
|
|
<div>${record.clockOut}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>工作时长</label>
|
|
|
<div>${record.hours}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>作业里程</label>
|
|
|
<div>${record.distance}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>考勤状态</label>
|
|
|
<div><span class="status-badge ${getAttendanceStatusClass(record.status)}">
|
|
|
${getAttendanceStatusText(record.status)}
|
|
|
</span></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button class="btn btn-primary" onclick="editAttendance('${record.id}')">编辑</button>
|
|
|
</div>
|
|
|
`
|
|
|
);
|
|
|
document.body.appendChild(modal);
|
|
|
}
|
|
|
|
|
|
function editAttendance(id) {
|
|
|
const record = attendanceData.find(a => a.id === id);
|
|
|
if (!record) return;
|
|
|
|
|
|
const person = personnelData.find(p => p.id === record.personnelId);
|
|
|
if (!person) return;
|
|
|
|
|
|
const modal = createEditModal(
|
|
|
`编辑考勤 - ${person.name} (${record.personnelId})`,
|
|
|
`
|
|
|
<form id="editAttendanceForm">
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>日期</label>
|
|
|
<input type="date" name="date" value="${record.date}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>上班打卡</label>
|
|
|
<input type="time" name="clockIn" value="${record.clockIn === '--' ? '' : record.clockIn}">
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>下班打卡</label>
|
|
|
<input type="time" name="clockOut" value="${record.clockOut === '--' ? '' : record.clockOut}">
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>考勤状态</label>
|
|
|
<select name="status" required>
|
|
|
<option value="normal" ${record.status === 'normal' ? 'selected' : ''}>正常</option>
|
|
|
<option value="late" ${record.status === 'late' ? 'selected' : ''}>迟到</option>
|
|
|
<option value="absent" ${record.status === 'absent' ? 'selected' : ''}>缺勤</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>作业里程</label>
|
|
|
<input type="text" name="distance" value="${record.distance}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>工作时长</label>
|
|
|
<input type="text" name="hours" value="${record.hours}" required>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button type="button" class="btn" onclick="closeModal(this)">取消</button>
|
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
`
|
|
|
);
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
// 表单提交处理
|
|
|
document.getElementById('editAttendanceForm').addEventListener('submit', function (e) {
|
|
|
e.preventDefault();
|
|
|
alert('考勤记录已更新');
|
|
|
closeModal(document.querySelector('.modal-overlay'));
|
|
|
loadAttendanceData();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function getAttendanceStatusClass(status) {
|
|
|
return {
|
|
|
'normal': 'status-normal',
|
|
|
'late': 'status-late',
|
|
|
'absent': 'status-absent'
|
|
|
}[status] || '';
|
|
|
}
|
|
|
|
|
|
// 绩效操作函数
|
|
|
function viewPerformanceDetails(id) {
|
|
|
const record = performanceData.find(p => p.id === id);
|
|
|
if (!record) return;
|
|
|
|
|
|
const person = personnelData.find(p => p.id === record.personnelId);
|
|
|
if (!person) return;
|
|
|
|
|
|
const modal = createDetailsModal(
|
|
|
`绩效详情 - ${person.name} (${record.personnelId})`,
|
|
|
`
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>考核周期</label>
|
|
|
<div>${record.period}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>出勤率</label>
|
|
|
<div>${record.attendanceRate}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>作业时长</label>
|
|
|
<div>${record.workHours}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>作业里程</label>
|
|
|
<div>${record.workDistance}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>任务完成率</label>
|
|
|
<div>${record.completionRate}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>质量评分</label>
|
|
|
<div>${record.qualityScore}</div>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>综合评分</label>
|
|
|
<div><strong style="color: ${getScoreColor(record.totalScore)}; font-size: 18px;">
|
|
|
${record.totalScore}
|
|
|
</strong></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button class="btn btn-primary" onclick="editPerformance('${record.id}')">编辑</button>
|
|
|
</div>
|
|
|
`
|
|
|
);
|
|
|
document.body.appendChild(modal);
|
|
|
}
|
|
|
|
|
|
function editPerformance(id) {
|
|
|
const record = performanceData.find(p => p.id === id);
|
|
|
if (!record) return;
|
|
|
|
|
|
const person = personnelData.find(p => p.id === record.personnelId);
|
|
|
if (!person) return;
|
|
|
|
|
|
const modal = createEditModal(
|
|
|
`编辑绩效 - ${person.name} (${record.personnelId})`,
|
|
|
`
|
|
|
<form id="editPerformanceForm">
|
|
|
<div class="form-grid">
|
|
|
<div class="form-group">
|
|
|
<label>考核周期</label>
|
|
|
<input type="text" name="period" value="${record.period}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>出勤率</label>
|
|
|
<input type="text" name="attendanceRate" value="${record.attendanceRate}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>作业时长</label>
|
|
|
<input type="text" name="workHours" value="${record.workHours}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>作业里程</label>
|
|
|
<input type="text" name="workDistance" value="${record.workDistance}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>任务完成率</label>
|
|
|
<input type="text" name="completionRate" value="${record.completionRate}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>质量评分</label>
|
|
|
<input type="text" name="qualityScore" value="${record.qualityScore}" required>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>综合评分</label>
|
|
|
<input type="text" name="totalScore" value="${record.totalScore.replace('分', '')}" required>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button type="button" class="btn" onclick="closeModal(this)">取消</button>
|
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
`
|
|
|
);
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
// 表单提交处理
|
|
|
document.getElementById('editPerformanceForm').addEventListener('submit', function (e) {
|
|
|
e.preventDefault();
|
|
|
alert('绩效记录已更新');
|
|
|
closeModal(document.querySelector('.modal-overlay'));
|
|
|
loadPerformanceData();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 通用模态框函数
|
|
|
function createDetailsModal(title, content) {
|
|
|
const modal = document.createElement('div');
|
|
|
modal.className = 'modal-overlay';
|
|
|
modal.innerHTML = `
|
|
|
<div class="modal-content" style="max-width: 600px;">
|
|
|
<div class="modal-header">
|
|
|
<h3>${title}</h3>
|
|
|
<button class="modal-close" onclick="closeModal(this)">×</button>
|
|
|
</div>
|
|
|
<div class="modal-body">
|
|
|
${content}
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
return modal;
|
|
|
}
|
|
|
|
|
|
function createEditModal(title, content) {
|
|
|
const modal = document.createElement('div');
|
|
|
modal.className = 'modal-overlay';
|
|
|
modal.innerHTML = `
|
|
|
<div class="modal-content" style="max-width: 700px;">
|
|
|
<div class="modal-header">
|
|
|
<h3>${title}</h3>
|
|
|
<button class="modal-close" onclick="closeModal(this)">×</button>
|
|
|
</div>
|
|
|
<div class="modal-body">
|
|
|
${content}
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
return modal;
|
|
|
}
|
|
|
|
|
|
function closeModal(element) {
|
|
|
const modal = element.closest('.modal-overlay');
|
|
|
modal.remove();
|
|
|
}
|
|
|
|
|
|
function closeModal2(modalId) {
|
|
|
const modal = document.getElementById(modalId);
|
|
|
if (modal) {
|
|
|
modal.style.display = 'none';
|
|
|
}
|
|
|
}
|
|
|
// 表单提交
|
|
|
document.getElementById('personnelForm').addEventListener('submit', function (e) {
|
|
|
e.preventDefault();
|
|
|
alert('人员档案创建成功!');
|
|
|
closePersonnelModal();
|
|
|
loadPersonnelData();
|
|
|
});
|
|
|
|
|
|
document.getElementById('scheduleForm').addEventListener('submit', function (e) {
|
|
|
e.preventDefault();
|
|
|
alert('排班计划创建成功!');
|
|
|
closeScheduleModal();
|
|
|
loadGridData();
|
|
|
});
|
|
|
|
|
|
// 导出报表
|
|
|
function exportReport() {
|
|
|
alert('导出人员报表功能将在新窗口打开');
|
|
|
// 实际应用中这里会生成并下载报表
|
|
|
}
|
|
|
|
|
|
// 页面加载完成后初始化
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
|
// 初始化加载概览数据
|
|
|
loadOverviewData();
|
|
|
|
|
|
// 加载侧边栏数据
|
|
|
loadSidebarData();
|
|
|
|
|
|
// 设置筛选器事件监听
|
|
|
setupFilterEvents();
|
|
|
setupAttendanceFilterEvents();
|
|
|
setupPerformanceFilterEvents();
|
|
|
|
|
|
// 绘制岗位分布图表
|
|
|
drawPositionChart();
|
|
|
});
|
|
|
|
|
|
// 加载侧边栏数据
|
|
|
function loadSidebarData() {
|
|
|
const attendanceAlerts = document.getElementById('attendanceAlerts');
|
|
|
const scheduleReminders = document.getElementById('scheduleReminders');
|
|
|
|
|
|
if (attendanceAlerts) {
|
|
|
attendanceAlerts.innerHTML = `
|
|
|
<div class="alert-item">
|
|
|
<div class="alert-title">李四 (CL002) 迟到</div>
|
|
|
<div class="alert-info">
|
|
|
<span>2024-01-17 08:15</span>
|
|
|
<span>迟到15分钟</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="alert-item">
|
|
|
<div class="alert-title">赵六 (CL006) 未打卡</div>
|
|
|
<div class="alert-info">
|
|
|
<span>2024-01-17</span>
|
|
|
<span>缺勤</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
if (scheduleReminders) {
|
|
|
scheduleReminders.innerHTML = `
|
|
|
<div class="alert-item">
|
|
|
<div class="alert-title">明日早班排班</div>
|
|
|
<div class="alert-info">
|
|
|
<span>2024-01-18 08:00</span>
|
|
|
<span>网格A1</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="alert-item">
|
|
|
<div class="alert-title">下周排班计划</div>
|
|
|
<div class="alert-info">
|
|
|
<span>2024-01-22</span>
|
|
|
<span>待确认</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 点击模态框外部关闭
|
|
|
window.onclick = function (event) {
|
|
|
const modals = document.querySelectorAll('.modal-overlay');
|
|
|
modals.forEach(modal => {
|
|
|
if (event.target === modal) {
|
|
|
modal.remove();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 网格数据
|
|
|
let gridData1 = [
|
|
|
{ id: 'grid1', name: '网格A1', area: '0.8km²', description: '主要负责A区东侧清洁', personnel: ['CL001', 'SV001', 'CL006'], stats: { distance: '6.2km', completion: '95%' } },
|
|
|
{ id: 'grid2', name: '网格A2', area: '0.9km²', description: '主要负责A区西侧清洁', personnel: ['CL002'], stats: { distance: '3.1km', completion: '68%' } },
|
|
|
{ id: 'grid3', name: '网格B1', area: '1.2km²', description: '主要负责B区北侧清洁', personnel: ['CL007'], stats: { distance: '8.7km', completion: '92%' } },
|
|
|
{ id: 'grid4', name: '网格B2', area: '1.1km²', description: '主要负责B区南侧清洁', personnel: [], stats: { distance: '0km', completion: '0%' } }
|
|
|
];
|
|
|
|
|
|
// 智能分配
|
|
|
function optimizeAllocation() {
|
|
|
showNotification('正在进行智能分配...', 'success');
|
|
|
|
|
|
// 模拟智能分配过程
|
|
|
setTimeout(() => {
|
|
|
// 随机分配人员到网格
|
|
|
gridData1.forEach(grid => {
|
|
|
// 清空当前分配
|
|
|
grid.personnel = [];
|
|
|
|
|
|
// 随机分配1-3人
|
|
|
const randomCount = Math.floor(Math.random() * 3) + 1;
|
|
|
const availablePersonnel = personnelData.filter(p =>
|
|
|
p.position === 'cleaner' &&
|
|
|
!gridData1.some(g => g.id !== grid.id && g.personnel.includes(p.id))
|
|
|
);
|
|
|
|
|
|
// 随机选择人员
|
|
|
for (let i = 0; i < randomCount && i < availablePersonnel.length; i++) {
|
|
|
const randomIndex = Math.floor(Math.random() * availablePersonnel.length);
|
|
|
grid.personnel.push(availablePersonnel[randomIndex].id);
|
|
|
availablePersonnel.splice(randomIndex, 1);
|
|
|
}
|
|
|
|
|
|
// 更新网格统计
|
|
|
grid.stats.distance = (Math.random() * 10 + 1).toFixed(1) + 'km';
|
|
|
grid.stats.completion = Math.floor(Math.random() * 30 + 70) + '%';
|
|
|
});
|
|
|
|
|
|
// 更新显示
|
|
|
loadGridData();
|
|
|
showNotification('智能分配完成', 'success');
|
|
|
}, 1500);
|
|
|
}
|
|
|
|
|
|
// 网格管理
|
|
|
function manageGrids() {
|
|
|
document.getElementById('gridManageModal').style.display = 'block';
|
|
|
loadGridTable();
|
|
|
}
|
|
|
|
|
|
// 加载网格表格数据
|
|
|
function loadGridTable() {
|
|
|
const gridTableBody = document.getElementById('gridTableBody');
|
|
|
if (!gridTableBody) return;
|
|
|
|
|
|
gridTableBody.innerHTML = gridData1.map(grid => `
|
|
|
<tr>
|
|
|
<td>${grid.id}</td>
|
|
|
<td>${grid.name}</td>
|
|
|
<td>${grid.area}</td>
|
|
|
<td>${grid.description}</td>
|
|
|
<td>
|
|
|
<button class="btn btn-primary" onclick="editGrid('${grid.id}')">编辑</button>
|
|
|
<button class="btn btn-danger" onclick="deleteGrid('${grid.id}')">删除</button>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`).join('');
|
|
|
}
|
|
|
|
|
|
// 显示新增网格表单
|
|
|
function showAddGridForm() {
|
|
|
document.getElementById('gridFormTitle').textContent = '新增网格';
|
|
|
document.getElementById('gridForm').reset();
|
|
|
document.getElementById('gridId').value = '';
|
|
|
document.getElementById('gridName').value = '';
|
|
|
document.getElementById('gridArea').value = '';
|
|
|
document.getElementById('gridDescription').value = '';
|
|
|
|
|
|
document.getElementById('gridManageModal').style.display = 'none';
|
|
|
document.getElementById('gridFormModal').style.display = 'block';
|
|
|
}
|
|
|
|
|
|
// 编辑网格
|
|
|
function editGrid(id) {
|
|
|
const grid = gridData1.find(g => g.id === id);
|
|
|
if (!grid) return;
|
|
|
|
|
|
document.getElementById('gridFormTitle').textContent = '编辑网格';
|
|
|
document.getElementById('gridId').value = grid.id;
|
|
|
document.getElementById('gridName').value = grid.name;
|
|
|
document.getElementById('gridArea').value = grid.area;
|
|
|
document.getElementById('gridDescription').value = grid.description;
|
|
|
|
|
|
document.getElementById('gridManageModal').style.display = 'none';
|
|
|
document.getElementById('gridFormModal').style.display = 'block';
|
|
|
}
|
|
|
|
|
|
// 删除网格
|
|
|
function deleteGrid(id) {
|
|
|
if (confirm('确定要删除此网格吗?删除后无法恢复!')) {
|
|
|
const index = gridData1.findIndex(g => g.id === id);
|
|
|
if (index !== -1) {
|
|
|
gridData1.splice(index, 1);
|
|
|
loadGridTable();
|
|
|
loadGridData(); // 更新网格分配显示
|
|
|
showNotification('网格删除成功', 'success');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 保存网格表单
|
|
|
document.getElementById('gridForm').addEventListener('submit', function (e) {
|
|
|
e.preventDefault();
|
|
|
|
|
|
const id = document.getElementById('gridId').value;
|
|
|
const name = document.getElementById('gridName').value;
|
|
|
const area = document.getElementById('gridArea').value;
|
|
|
const description = document.getElementById('gridDescription').value;
|
|
|
|
|
|
if (id) {
|
|
|
// 编辑现有网格
|
|
|
const grid = gridData1.find(g => g.id === id);
|
|
|
if (grid) {
|
|
|
grid.name = name;
|
|
|
grid.area = area;
|
|
|
grid.description = description;
|
|
|
showNotification('网格信息已更新', 'success');
|
|
|
}
|
|
|
} else {
|
|
|
// 新增网格
|
|
|
const newId = 'grid' + (gridData1.length + 1);
|
|
|
gridData1.push({
|
|
|
id: newId,
|
|
|
name: name,
|
|
|
area: area,
|
|
|
description: description,
|
|
|
personnel: [],
|
|
|
stats: { distance: '0km', completion: '0%' }
|
|
|
});
|
|
|
showNotification('网格添加成功', 'success');
|
|
|
}
|
|
|
|
|
|
// 关闭表单模态框,重新打开网格管理模态框
|
|
|
document.getElementById('gridFormModal').style.display = 'none';
|
|
|
document.getElementById('gridManageModal').style.display = 'block';
|
|
|
|
|
|
// 刷新数据
|
|
|
loadGridTable();
|
|
|
loadGridData();
|
|
|
});
|
|
|
|
|
|
// // 加载网格分配数据
|
|
|
// function loadGridData() {
|
|
|
// const gridAllocationGrid = document.getElementById('gridAllocationGrid');
|
|
|
// if (!gridAllocationGrid) return;
|
|
|
|
|
|
// gridAllocationGrid.innerHTML = gridData1.map(grid => {
|
|
|
// const personnelList = grid.personnel.map(id => {
|
|
|
// const person = personnelData.find(p => p.id === id);
|
|
|
// if (!person) return '';
|
|
|
|
|
|
// return `
|
|
|
// <div class="personnel-info">
|
|
|
// <div class="personnel-icon"><i class="fas fa-user"></i></div>
|
|
|
// <div class="personnel-details">
|
|
|
// <h4>${person.name}</h4>
|
|
|
// <p>${person.id} - ${getPositionText(person.position)}</p>
|
|
|
// </div>
|
|
|
// <span class="status-badge ${person.status === 'online' ? 'status-online' : 'status-offline'}">
|
|
|
// ${person.status === 'online' ? '在岗' : '离岗'}
|
|
|
// </span>
|
|
|
// </div>
|
|
|
// `;
|
|
|
// }).join('');
|
|
|
|
|
|
// return `
|
|
|
// <div class="personnel-item">
|
|
|
// <div class="card-title">
|
|
|
// <i class="fas fa-th"></i>
|
|
|
// ${grid.name} (${grid.area})
|
|
|
// </div>
|
|
|
// ${personnelList || '<p style="color: #4a6fa5; text-align: center;">暂无人员分配</p>'}
|
|
|
// <div class="personnel-params">
|
|
|
// <div class="param-item">
|
|
|
// <span class="param-value">${grid.stats.distance}</span>
|
|
|
// <div class="param-label">今日作业</div>
|
|
|
// </div>
|
|
|
// <div class="param-item">
|
|
|
// <span class="param-value">${grid.stats.completion}</span>
|
|
|
// <div class="param-label">完成率</div>
|
|
|
// </div>
|
|
|
// </div>
|
|
|
// <div style="margin-top: 15px;">
|
|
|
// <button class="btn btn-primary" onclick="manageGridPersonnel('${grid.id}')">管理</button>
|
|
|
// </div>
|
|
|
// </div>
|
|
|
// `;
|
|
|
// }).join('');
|
|
|
// }
|
|
|
|
|
|
// 管理网格人员
|
|
|
function manageGridPersonnel(gridId) {
|
|
|
const grid = gridData1.find(g => g.id === gridId);
|
|
|
if (!grid) return;
|
|
|
|
|
|
const modal = createDetailsModal(
|
|
|
`管理网格人员 - ${grid.name}`,
|
|
|
`
|
|
|
<div style="max-height: 400px; overflow-y: auto;">
|
|
|
${personnelData.map(person => `
|
|
|
<div style="margin-bottom: 10px; padding: 10px; background: #f5f9ff; border-radius: 6px;
|
|
|
display: flex; align-items: center; justify-content: space-between;">
|
|
|
<div>
|
|
|
<div style="font-weight: 500; color: #1a4b8c;">${person.name} (${person.id})</div>
|
|
|
<div style="font-size: 12px; color: #4a6fa5;">
|
|
|
${getPositionText(person.position)} | ${getGridText(person.grid)}
|
|
|
</div>
|
|
|
</div>
|
|
|
<label style="display: flex; align-items: center; gap: 8px;">
|
|
|
<input type="checkbox" ${grid.personnel.includes(person.id) ? 'checked' : ''}
|
|
|
onchange="toggleGridAssignment('${gridId}', '${person.id}', this.checked)">
|
|
|
<span style="font-size: 12px;">分配</span>
|
|
|
</label>
|
|
|
</div>
|
|
|
`).join('')}
|
|
|
</div>
|
|
|
<div style="text-align: right; margin-top: 20px;">
|
|
|
<button type="button" class="btn" onclick="closeModal(this)">完成</button>
|
|
|
</div>
|
|
|
`
|
|
|
);
|
|
|
document.body.appendChild(modal);
|
|
|
}
|
|
|
|
|
|
// 切换网格分配
|
|
|
function toggleGridAssignment(gridId, personnelId, assign) {
|
|
|
const grid = gridData1.find(g => g.id === gridId);
|
|
|
if (!grid) return;
|
|
|
|
|
|
if (assign) {
|
|
|
if (!grid.personnel.includes(personnelId)) {
|
|
|
grid.personnel.push(personnelId);
|
|
|
// 从其他网格中移除该人员
|
|
|
gridData1.forEach(g => {
|
|
|
if (g.id !== gridId && g.personnel.includes(personnelId)) {
|
|
|
g.personnel = g.personnel.filter(id => id !== personnelId);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
} else {
|
|
|
grid.personnel = grid.personnel.filter(id => id !== personnelId);
|
|
|
}
|
|
|
|
|
|
// 更新网格数据
|
|
|
loadGridData();
|
|
|
}
|
|
|
|
|
|
// 显示通知
|
|
|
function showNotification(message, type = 'success') {
|
|
|
const notification = document.createElement('div');
|
|
|
notification.className = `notification ${type}`;
|
|
|
notification.textContent = message;
|
|
|
document.body.appendChild(notification);
|
|
|
|
|
|
setTimeout(() => {
|
|
|
notification.remove();
|
|
|
}, 3000);
|
|
|
}
|
|
|
</script>
|
|
|
</body>
|
|
|
|
|
|
</html> |