console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>倒班排班日历|工作日PR|周末节假日SX交路</title>
<style>
*{
margin:0;padding:0;box-sizing:border-box;
font-family: system-ui, -apple-system, sans-serif;
}
body{
background:#f4f5f7;
padding:16px;
min-height:100vh;
}
.header{
text-align:center;
margin-bottom:20px;
}
.title{
font-size:24px;font-weight:bold;color:#222;
}
.desc{
font-size:14px;color:#666;margin-top:6px;
}
.rule-tip{
background:#fff;border-radius:12px;padding:14px;margin-bottom:16px;
font-size:13px;color:#444;
line-height:1.6;
}
.month-wrap{
background:#fff;border-radius:12px;padding:12px;margin-bottom:20px;
}
.month-title{
text-align:center;font-size:18px;font-weight:bold;margin:10px 0;
}
.week-row{
display:grid;
grid-template-columns:repeat(7,1fr);
gap:4px;margin-bottom:8px;
}
.week-cell{
text-align:center;font-size:13px;font-weight:bold;color:#555;padding:6px 0;
}
.date-grid{
display:grid;
grid-template-columns:repeat(7,1fr);
gap:4px;
}
.day-cell{
min-height:64px;background:#f8f8f8;border-radius:8px;padding:4px;
display:flex;flex-direction:column;justify-content:space-between;
cursor:pointer;
}
.day-cell.today{
border:2px solid #2b3440;
}
.day-num{
font-size:12px;font-weight:bold;
}
.shift-name{
font-size:11px;color:#fff;padding:2px 2px;border-radius:4px;text-align:center;
}
.tag-night{background:#3a1f22;}
.tag-middle{background:#ff784f;}
.tag-early{background:#8b5cf6;}
.tag-rest1{background:#74d08b;}
.tag-rest2{background:#56b86c;}
.modal-mask{
position:fixed;
inset:0;
background:rgba(0,0,0,0.5);
display:none;
padding:12px;
z-index:999;
}
.modal{
background:#fff;
border-radius:12px;
max-width:520px;
margin:0 auto;
max-height:85vh;
display:flex;
flex-direction:column;
}
.modal-head{
padding:16px 16px 8px 16px;
}
.modal-title{
font-size:18px;font-weight:bold;margin-bottom:8px;
}
.modal-sub{
font-size:13px;color:#666;
}
.modal-scroll{
padding:0 16px;
overflow-y:auto;
-webkit-overflow-scrolling:touch;
flex-shrink:1;
}
.modal-foot{
padding:12px 16px 16px 16px;
}
.close-btn{
width:100%;
background:#eee;
border:none;
border-radius:8px;
padding:10px;
font-size:14px;
}
.row-item{
background:#f7f7f7;
border-radius:6px;
padding:10px;
margin:8px 0;
font-size:12px;
line-height:1.6;
}
.empty-text{
color:#999;
font-size:13px;
text-align:center;
padding:24px 0;
}
</style>
</head>
<body>
<div class="header">
<div class="title">倒班排班日历</div>
<div class="desc">基准锚点:2026-10-22 = 夜19|工作日PR2601|周末/节假日SX交路</div>
</div>
<div class="rule-tip">
✅排班:1‑36号位循环,顺序:夜→中→早→休1→休2<br>
✅规则:周一‑周五:PR平日;周六/周日/法定节假日:SX双休交路
</div>
<div id="yearBox"></div>
<div class="modal-mask" id="mask">
<div class="modal">
<div class="modal-head">
<div class="modal-title" id="mTitle"></div>
<div class="modal-sub" id="mSub"></div>
</div>
<div class="modal-scroll" id="mContent"></div>
<div class="modal-foot">
<button class="close-btn" id="closeBtn">关闭</button>
</div>
</div>
</div>
<script>
//固定锚点 2026-10-22 = 夜19
const baseDate = new Date("2026-10-22");
const baseTotalIndex = (19-1)*5 + 0;
const singlePostShift = ["夜班","中班","早班","休息1","休息2"];
const postCount = 36;
const cycleDays = postCount * singlePostShift.length;
//节假日
const holidayList = [
"2026-01-01",
"2026-01-29","2026-01-30","2026-01-31","2026-02-01","2026-02-02","2026-02-03","2026-02-04",
"2026-04-04","2026-04-05","2026-04-06",
"2026-05-01","2026-05-02","2026-05-03","2026-05-04","2026-05-05",
"2026-06-19","2026-06-20","2026-06-21",
"2026-10-01","2026-10-02","2026-10-03","2026-10-04","2026-10-05","2026-10-06","2026-10-07","2026-10-08"
];
function isHoliday(d){
const s = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
return holidayList.includes(s);
}
function useSX(d){
const w = d.getDay();
return (w===0 || w===6) || isHoliday(d);
}
//根据日期算班次
function getShiftInfoByDate(targetDate){
const diffMs = targetDate.getTime() - baseDate.getTime();
const diffDay = Math.round(diffMs / (1000*60*60*24));
let totalIdx = (baseTotalIndex + diffDay) % cycleDays;
if(totalIdx <0) totalIdx += cycleDays;
let postNo = Math.floor(totalIdx / singlePostShift.length)+1;
let inIdx = totalIdx % singlePostShift.length;
let raw = singlePostShift[inIdx];
let showName,tagClass;
if(raw==="夜班"){ showName=`夜${postNo}`; tagClass="tag-night"; }
else if(raw==="中班"){ showName=`中${postNo}`; tagClass="tag-middle"; }
else if(raw==="早班"){ showName=`早${postNo}`; tagClass="tag-early"; }
else if(raw==="休息1"){ showName=`休1`; tagClass="tag-rest1"; }
else if(raw==="休息2"){ showName=`休2`; tagClass="tag-rest2"; }
return {postNo,raw,showName,tagClass,totalIdx,time:targetDate.getTime()};
}
//交路样板
const jiaoLuData = {
pr:{
"夜19":[
{出勤:"17:24",地点:"朱房北",接车:"朱房北17:44:29",表号:"60",车次:"2158",交车:"昌平东关18:19:16",公里:"28.2"},
{出勤:"",地点:"",接车:"昌平东关18:19:16",表号:"60",车次:"1197",交车:"蓟门桥19:16:22",公里:"37.5"}
]
},
sx:{
"夜19":[
{出勤:"17:34",地点:"朱房北",接车:"朱房北17:54:26",表号:"33",车次:"2100",交车:"昌平西山口18:39:31",公里:"34.6"}
]
}
};
function getJiaoLu(type,key){
const src = jiaoLuData[type]||{};
return src[key]||null;
}
//弹窗
const maskEl = document.getElementById("mask");
const mTitle = document.getElementById("mTitle");
const mSub = document.getElementById("mSub");
const mContent = document.getElementById("mContent");
const closeBtn = document.getElementById("closeBtn");
let savedScrollY = 0;
function openModal(timeMs,shiftObj){
savedScrollY = window.scrollY;
document.body.style.position = "fixed";
document.body.style.top = -savedScrollY + "px";
document.body.style.width = "100%";
maskEl.style.display="block";
const dateObj = new Date(timeMs);
const isSX = useSX(dateObj);
const dataType = isSX ? "sx" : "pr";
const typeText = isSX ? "SX双休交路(周末/节假日)" : "PR2601平日交路";
mTitle.innerText = `${dateObj.getFullYear()}年${dateObj.getMonth()+1}月${dateObj.getDate()}日 ${shiftObj.showName}`;
mSub.innerText = `交路数据源:${typeText}`;
const jl = getJiaoLu(dataType, shiftObj.showName);
if(!jl || jl.length===0){
mContent.innerHTML = `<div class="empty-text">暂无该位置交路数据</div>`;
return;
}
let html = "";
for(let r of jl){
html += `
<div class="row-item">
出勤:${r.出勤||"-"} ${r.地点||"-"}<br>
接车:${r.接车||"-"}|表号${r.表号||"-"}|车次${r.车次||"-"}<br>
交车:${r.交车||"-"}|公里${r.公里||"-"}
</div>
`;
}
mContent.innerHTML = html;
}
function closeModal(){
maskEl.style.display="none";
document.body.style.position = "";
document.body.style.top = "";
document.body.style.width = "";
window.scrollTo(0, savedScrollY);
}
closeBtn.addEventListener("click",closeModal);
maskEl.addEventListener("click",e=>{ if(e.target===maskEl) closeModal(); });
//日历点击事件委托
document.getElementById("yearBox").addEventListener("click",e=>{
const cell = e.target.closest(".day-cell");
if(!cell) return;
const time = Number(cell.dataset.time);
const shiftJson = cell.dataset.shift;
const shiftObj = JSON.parse(shiftJson);
openModal(time, shiftObj);
});
//渲染全年日历
function renderYear(){
const today = new Date();
const currYear = today.getFullYear();
const box = document.getElementById("yearBox");
let allHtml = "";
for(let m=0;m<12;m++){
let first = new Date(currYear,m,1);
let startWeek = first.getDay();
let daysCount = new Date(currYear,m+1,0).getDate();
let monthHtml = `
<div class="month-wrap">
<div class="month-title">${currYear}年${m+1}月</div>
<div class="week-row">
<div class="week-cell">日</div>
<div class="week-cell">一</div>
<div class="week-cell">二</div>
<div class="week-cell">三</div>
<div class="week-cell">四</div>
<div class="week-cell">五</div>
<div class="week-cell">六</div>
</div>
<div class="date-grid">
`;
for(let i=0;i<startWeek;i++){
monthHtml += `<div class="day-cell"></div>`;
}
for(let d=1;d<=daysCount;d++){
const cd = new Date(currYear,m,d);
const si = getShiftInfoByDate(cd);
const isToday = (cd.toDateString() === today.toDateString()) ? "today" : "";
monthHtml += `
<div class="day-cell ${isToday}" data-time="${cd.getTime()}" data-shift='${JSON.stringify(si)}'>
<div class="day-num">${d}</div>
<div class="shift-name ${si.tagClass}">${si.showName}</div>
</div>
`;
}
monthHtml += `</div></div>`;
allHtml += monthHtml;
}
box.innerHTML = allHtml;
}
renderYear();
</script>
</body>
</html>