<!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:不知道 / 找不到

我可以 直接告诉你该在哪一层“下刀”
不用你再多熬一天。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐