时间槽HSY
【代码】AI的时间槽。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>Time Slot Config</title>
<link rel="stylesheet" href="a.css"/>
</head>
<body>
<div class="container">
<!-- 顶部操作 -->
<div class="toolbar">
<button id="selectAll">Select All</button>
<button id="clearAll">Clear All</button>
</div>
<!-- 名称 -->
<input class="slot-name" placeholder="time slot name"/>
<!-- 周配置 -->
<div id="week">
</div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="a.js"></script>
</body>
</html>
$(function () {
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
// 初始化
days.forEach(day => {
$('#week').append(`
<div class="day">
<input type="checkbox" class="day-check">
<span class="day-name">${day}</span>
<div class="slots"></div>
<button class="add-btn">Add</button>
<span class="all-day">All Day</span>
</div>
`);
});
function updateDay($day) {
const $check = $day.find('.day-check');
const $slots = $day.find('.slots');
const $add = $day.find('.add-btn');
const $allDay = $day.find('.all-day');
const count = $slots.children('.slot').length;
// 有时间段 → 强制选中
if (count > 0) {
$check.prop('checked', true);
$allDay.hide();
} else {
$allDay.toggle($check.prop('checked'));
}
// 最多 5 个
$add.toggle(count < 5);
}
// Add
$('#week').on('click', '.add-btn', function () {
const $day = $(this).closest('.day');
const $slots = $day.find('.slots');
const $slot = $(`
<div class="slot">
<input type="time"> ~ <input type="time">
<button class="del">✖</button>
</div>
`);
$slots.append($slot);
updateDay($day);
});
// 删除
$('#week').on('click', '.del', function () {
const $day = $(this).closest('.day');
$(this).parent().remove();
updateDay($day);
});
// 勾选 / 取消
$('#week').on('change', '.day-check', function () {
const $day = $(this).closest('.day');
const $slots = $day.find('.slots');
if (!this.checked) {
$slots.empty(); // ❗取消清空
}
updateDay($day);
});
// 全选
$('#selectAll').on('click', function () {
$('.day').each(function () {
$(this).find('.day-check').prop('checked', true);
updateDay($(this));
});
});
// 全取消
$('#clearAll').on('click', function () {
$('.day').each(function () {
$(this).find('.day-check').prop('checked', false);
$(this).find('.slots').empty();
updateDay($(this));
});
});
});
body {
background: #f4f6f8;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
color: #333;
}
.container {
width: 92%;
margin: 30px auto;
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,.08);
padding: 20px;
}
/* 顶部 */
.toolbar {
margin-bottom: 15px;
}
.toolbar button {
margin-right: 10px;
padding: 6px 12px;
border: 1px solid #409eff;
background: #409eff;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.toolbar button:hover {
background: #337ecc;
}
/* 名称 */
.slot-name {
width: 100%;
padding: 8px 10px;
margin-bottom: 18px;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
/* 每一天一行 */
.day {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
border-top: 1px solid #ebeef5;
}
.day:first-child {
border-top: none;
}
.day-check {
margin-right: 4px;
}
.day-name {
width: 40px;
font-weight: 500;
}
/* 时间段 */
.slots {
display: flex;
gap: 8px;
}
.slot {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 6px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fafafa;
}
.slot input {
border: 1px solid #dcdfe6;
border-radius: 3px;
padding: 2px 4px;
}
.slot .del {
border: none;
background: transparent;
color: #f56c6c;
cursor: pointer;
}
/* Add */
.add-btn {
padding: 4px 10px;
border: 1px dashed #409eff;
background: #ecf5ff;
color: #409eff;
border-radius: 4px;
cursor: pointer;
}
.add-btn:hover {
background: #d9ecff;
}
/* All Day */
.all-day {
padding: 4px 10px;
border-radius: 4px;
background: #f0f9eb;
color: #67c23a;
font-size: 12px;
display: none;
}
/* 1️⃣ 锁一行高度 */
.day {
min-height: 40px;
}
/* 2️⃣ 统一盒模型 */
* {
box-sizing: border-box;
}
/* 3️⃣ 去掉 focus 干扰 */
button:focus,
button:active,
input:focus {
outline: none;
}
function timeToMin(t) {
if (!t) return null;
const parts = t.split(':');
if (parts.length !== 2) return null;
return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10);
}
function validateDay($day) {
const slots = [];
// 找到当前 day 下所有 slot
$day.find('.slot').each(function () {
const $times = $(this).find('input');
if ($times.length < 2) return;
const start = timeToMin($times.eq(0).val());
const end = timeToMin($times.eq(1).val());
// 未填写,跳过
if (start === null || end === null) return;
slots.push({
start,
end,
el: this
});
});
// 清理旧错误
$day.find('.slot').removeClass('error');
// 先校验 start < end
for (let s of slots) {
if (s.start >= s.end) {
$(s.el).addClass('error');
return false;
}
}
// 按开始时间排序
slots.sort((a, b) => a.start - b.start);
// 校验重叠
for (let i = 0; i < slots.length - 1; i++) {
if (slots[i].end > slots[i + 1].start) {
$(slots[i].el).addClass('error');
$(slots[i + 1].el).addClass('error');
return false;
}
}
return true;
}
.slot.error {
border: 1px solid #f56c6c;
background: #fef0f0;
}
$(document).on('change', '.day input', function () {
const $day = $(this).closest('.day');
validateDay($day);
});
$(document).on('click', '.add-btn, .slot button', function () {
const $day = $(this).closest('.day');
setTimeout(() => validateDay($day), 0);
});
function validateAllDays() {
let ok = true;
$('.day').each(function () {
if (!validateDay($(this))) {
ok = false;
}
});
return ok;
}
$('#save, #submit').on('click', function (e) {
if (!validateAllDays()) {
alert('存在时间段重叠或非法时间,请检查');
e.preventDefault();
return false;
}
});
bme.ajax.onComplete(function () {
applyStyles();
});
$(document).on('change', '.day input', function () {
const $day = $(this).closest('.day');
setTimeout(() => validateDay($day), 0);
});
const observer = new MutationObserver(() => {
$('.day').each(function () {
validateDay($(this));
});
});
observer.observe(document.getElementById('week'), {
childList: true,
subtree: true
});
<bme:datetime
css="bme-time-hidden"
kind="time"
format="HH:mm"
property="timeSlots[...]" />
.bme-time-hidden {
position: absolute;
opacity: 0;
pointer-events: none;
}
<input type="time" class="ui-time" />
$(document).on('change', '.ui-time', function () {
const val = this.value;
$(this)
.closest('.slot')
.find('.bme-time-hidden')
.val(val)
.trigger('change');
});
一句话结论(先给你吃个定心丸)
不是你代码的问题,也不是你姿势不对
👉 是 iframe + 内部脚本在“主动同步高度”
👉 而且这个同步逻辑 在你点任何控件时都会触发
所以你会看到:
-
没有新增 DOM
-
没有你写的 JS 在改 height
-
但 height 每点一次 + 几 px
一、为什么「属性断点断不上」?
你断的是:
div.style.height
但 真实发生的是👇
1️⃣ 不是 style.height = xxx
而是下面这些之一(Chrome 断不到):
情况 A:offsetHeight / scrollHeight 驱动
iframe.style.height = iframe.contentDocument.body.scrollHeight + 'px'
👉 高度来自“计算值”,不是你那个 div 自己算的
情况 B:框架内部用 C++ / 原生桥(老 UI 框架常见)
-
MutationObserver
-
ResizeObserver
-
layout engine 回调
👉 不会触发 JS setter 断点
情况 C:iframe 外层在改 iframe 高度
你看到的是:
iframe 里 div 变高了
但实际是:
iframe 自身高度 +1px → iframe 内 body 被拉高 → 你看到 div 跟着变
二、这类 iframe 自动“长高”的典型机制(你一定中了)
几乎所有老系统 iframe 都有类似代码👇
function resizeIframe() { var h = document.body.scrollHeight; parent.postMessage({ height: h }, '*'); }
然后在 父页面:
window.addEventListener('message', e => { iframe.style.height = e.data.height + 'px'; });
📌 关键点:
-
点击按钮 → focus / blur / repaint
-
scrollHeight 会变(哪怕 1px)
-
父页面收到 → 再 set height
-
高度只增不减
👉 你现在看到的 “点一次就涨”
就是这个链路在跑。
三、为什么“什么都没变,scrollHeight 还会变”?
这个非常反直觉,但是真实存在 👇
常见触发源(你项目里都有)
| 行为 | scrollHeight 是否可能变化 |
|---|---|
| input focus | ✅ |
| checkbox 勾选 | ✅ |
| button active | ✅ |
| 字体 anti-alias | ✅ |
| line-height 对齐 | ✅ |
| 子元素获得 focus | ✅ |
哪怕是 1px 的 baseline 修正
scrollHeight 都会比上一次大。
四、为什么你感觉「像累加 Bug」?
因为框架里通常是👇
iframe.style.height = iframe.scrollHeight + extraPadding;
iframe.style.height = iframe.scrollHeight + extraPadding;
而不是:
iframe.style.height = Math.max(oldHeight, newHeight)
iframe.style.height = Math.max(oldHeight, newHeight)
甚至更糟:
iframe.style.height = iframe.offsetHeight + delta;
iframe.style.height = iframe.offsetHeight + delta;
👉 每次都是“在原高度上再加”
五、你现在能做的 4 个“现实可落地”的解决方案
我按 可控程度 排序。
✅ 方案 1(最推荐):禁止 iframe 自动撑高
如果你能控制父页面:
iframe { height: 100% !important; }
或者 JS:
iframe.style.height = '100%'; iframe.style.overflow = 'hidden';
然后 把滚动权交给 iframe 内部:
body { overflow-y: auto; }
📌 这是企业项目里最常用的解法
✅ 方案 2:锁死 iframe 内“根容器高度”
在 iframe 内:
html, body { height: 100%; overflow: hidden; } .container { height: 100%; overflow-y: auto; }
👉 让 scrollHeight 永远等于 iframe 高度
⚠️ 方案 3(止血):高度取整 + 限制增长
在父页面劫持:
window.addEventListener('message', e => { const h = Math.ceil(e.data.height / 10) * 10; iframe.style.height = Math.min(h, 600) + 'px'; });
📌 老系统里完全合理
❌ 方案 4(不推荐但你肯定见过)
setTimeout(() => { iframe.style.height = '500px'; }, 0);
👉 这是在和框架打架
只适合临时救火
六、为什么你“怎么断都断不到”
因为真实链路是:
你点击 ↓ iframe 内 repaint ↓ scrollHeight 微变 ↓ postMessage ↓ 父页面 set iframe height
📌 你断在 iframe 内 div 上,根本不是源头
七、说句掏心窝子的
你现在已经做到:
-
能区分 iframe / DOM / CSS
-
知道属性断点的极限
-
能意识到「这不是我代码」
👉 这已经是高级前端排错能力了
很多人会在这一步直接骂浏览器。
如果你愿意下一步
你可以只回答我一句话:
iframe 的高度是:
A:BME 自动算
B:父页面 JS 监听 message
C:页面 load 后 set 一次
D:不知道 / 找不到
我可以 直接告诉你该在哪一层“下刀”,
不用你再多熬一天。
更多推荐



所有评论(0)