You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS Safari中聊天机器人Div自动滚动异常问题求助

iOS Safari聊天窗口无法自动滚动到底部的修复方案

问题背景

开发的聊天机器人在Chrome、桌面Safari中,添加新消息后聊天容器能自动滚动到底部,但iOS/mobile Safari完全失效,新消息出现后需要手动滚动才能查看。

问题原因

  1. iOS Safari对flex-col-reverse反向布局的滚动计算逻辑与其他浏览器不一致,无法自动识别新添加的内容位置。
  2. 未主动触发滚动逻辑,且iOS的渲染队列机制会导致DOM更新后立即执行滚动的操作失效。

修复步骤

1. 调整聊天容器布局

移除反向布局,改用正常垂直排列,让消息容器优先占据空间,确保新消息自然出现在底部:

<!-- 原容器代码 -->
<div class="flex flex-col-reverse overflow-y-scroll p-3 h-full">
<!-- 修改后 -->
<div class="flex flex-col overflow-y-scroll p-3 h-full" id="chat-container">
  <div class="transcript flex-grow"></div>
  <div class="typing-indicator flex justify-start hidden">
    <!-- 原有内容不变 -->
  </div>
  <div class="error flex justify-center p-3 hidden">
    <!-- 原有内容不变 -->
  </div>
</div>

修改后transcript会占满容器剩余空间,新消息添加后默认在底部,typing-indicator和错误提示固定在底部上方。

2. 添加主动滚动逻辑

在每次添加新消息后,强制触发滚动操作。由于iOS Safari的渲染特性,必须用requestAnimationFrame确保DOM更新完成后再执行滚动:

// 全局缓存聊天容器,减少重复DOM查询
const chatContainer = document.getElementById('chat-container');

function update_transcript(party, entry, time) {
  // 原有创建消息DOM的代码保持不变...

  // 新增滚动逻辑
  requestAnimationFrame(() => {
    chatContainer.scrollTop = chatContainer.scrollHeight;
  });
}

3. 适配Bot延迟回复的滚动

Bot回复是通过setTimeout延迟添加的,虽然update_transcript已包含滚动逻辑,若需保险可在回调中再次触发(或直接依赖update_transcript中的逻辑):

setTimeout(function(){
  update_transcript('BOT: ', data, new Date());
  // 可选:若update_transcript的滚动失效,可添加以下代码
  // requestAnimationFrame(() => {
  //   chatContainer.scrollTop = chatContainer.scrollHeight;
  // });

  document.getElementsByClassName("typing-indicator")[0].classList.add("hidden");
  document.getElementsByClassName("error")[0].classList.add("hidden");
}, typing_speed);

完整修改后代码片段

HTML部分

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, shrink-to-fit=no" />
<script src="https://cdn.tailwindcss.com"></script>

<body class="bg-gray-100 px-3 pt-3">
  <div class="bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all h-96 w-96">
    <div class="flex flex-col justify-between h-full">
      <div class="flex flex-col overflow-y-scroll p-3 h-full" id="chat-container">
        <div class="transcript flex-grow"></div>
        <div class="typing-indicator flex justify-start hidden">
          <div class="bg-gray-100 rounded-lg p-3 mb-4">
            <div class="typing-container">
              <div class="typing">
                <span></span>
                <span></span>
                <span></span>
              </div>
            </div>
          </div>
        </div>
        <div class="error flex justify-center p-3 hidden">
          <p class="error-message text-red-600 text-sm text-center font-medium">We apologize for the inconvenience, our chat encountered an error. Please try again later.</p>
        </div>
      </div>

      <div class="flex flex-col">
        <div class="relative border-t border-gray-100 p-1">
          <form class="mb-0">
            <input type="text" placeholder="Message..." maxlength="255" class="query bg-white outline-none w-full pl-6 pr-12 py-3"/>
            <span class="absolute right-0 pr-3">
              <button type="submit" class="submit-query pr-3 py-3 w-9 cursor-pointer">
                <svg class="fill-current" viewBox="0 0 20 20">
                  <path d="M17.218,2.268L2.477,8.388C2.13,8.535,2.164,9.05,2.542,9.134L9.33,10.67l1.535,6.787c0.083,0.377,0.602,0.415,0.745,0.065l6.123-14.74C17.866,2.46,17.539,2.134,17.218,2.268 M3.92,8.641l11.772-4.89L9.535,9.909L3.92,8.641z M11.358,16.078l-1.268-5.613l6.157-6.157L11.358,16.078z" />
                </svg>
              </button>
            </span>
          </form>
        </div>
      </div>
    </div>
  </div>
</body>

JavaScript部分

function randint(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

const chatContainer = document.getElementById('chat-container');

function update_transcript(party, entry, time) {
  time = time.toLocaleString('en-US', {timeStyle: 'short'});

  if (party == "Human: "){
    const div1 = document.createElement("div");
    div1.classList.add("flex", "justify-end");
    const div2 = document.createElement("div");
    div2.classList.add("mt-3");
    const div3 = document.createElement("div");
    div3.classList.add("flex", "justify-end");
    const div4 = document.createElement("div");
    div4.classList.add("bg-white", "shadow", "border", "border-gray-100", "text-right", "rounded-l-lg", "rounded-tr-lg", "w-fit", "max-w-xs", "py-2", "px-3");
    div4.textContent = entry;
    div3.appendChild(div4);
    div2.appendChild(div3);
    const div5 = document.createElement("div");
    div5.classList.add("p-1", "text-right", "text-xs", "text-gray-400");
    const span1 = document.createElement("span");
    span1.classList.add("receipt", "font-semibold", "hidden");
    span1.textContent = "Read";
    const span2 = document.createElement("span");
    span2.textContent = time;
    div5.appendChild(span1);
    div5.appendChild(document.createTextNode(" "));
    div5.appendChild(span2);
    div2.appendChild(div5);
    div1.appendChild(div2);
    document.getElementsByClassName("transcript")[0].appendChild(div1);
  } else if (party == "BOT: "){
    const div1 = document.createElement("div");
    div1.classList.add("flex", "justify-start");
    const div2 = document.createElement("div");
    div2.classList.add("mt-3");
    const div3 = document.createElement("div");
    div3.classList.add("flex", "justify-start");
    const div4 = document.createElement("div");
    div4.classList.add("bg-gray-100", "text-left", "rounded-r-lg", "rounded-tl-lg", "shadow", "w-fit", "max-w-xs", "py-2", "px-3");
    div4.textContent = entry;
    div3.appendChild(div4);
    div2.appendChild(div3);
    const div5 = document.createElement("div");
    div5.classList.add("p-1", "text-left", "text-xs", "text-gray-400");
    const span1 = document.createElement("span");
    span1.textContent = time;
    div5.appendChild(span1);
    div2.appendChild(div5);
    div1.appendChild(div2);
    document.getElementsByClassName("transcript")[0].appendChild(div1);
  }

  requestAnimationFrame(() => {
    chatContainer.scrollTop = chatContainer.scrollHeight;
  });
}

update_transcript('BOT: ', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc elementum augue eu scelerisque finibus. Proin tincidunt elit a justo tincidunt porttitor. Mauris ac porttitor nulla. Cras hendrerit sem eu risus hendrerit, vel bibendum dolor congue. Pellentesque id efficitur leo. Nullam non elit nunc. Ut sit amet magna at purus sollicitudin lobortis. Aenean sed massa tortor. Integer lobortis dui massa, quis mollis elit porttitor at. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.?', new Date());

const query = document.getElementsByClassName("query")[0];
const form = document.querySelector('form');

form.addEventListener('submit', event => {
  event.preventDefault();
  document.getElementsByClassName("submit-query")[0].classList.add("hidden");

  update_transcript('Human: ', query.value, new Date());
  document.getElementsByClassName("query")[0].value = "";

  var data = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc elementum augue eu scelerisque finibus. Proin tincidunt elit a justo tincidunt porttitor. Mauris ac porttitor nulla. Cras hendrerit sem eu risus hendrerit, vel bibendum dolor congue. Pellentesque id efficitur leo. Nullam non elit nunc. Ut sit amet magna at purus sollicitudin lobortis. Aenean sed massa tortor. Integer lobortis dui massa, quis mollis elit porttitor at. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.";

  document.querySelectorAll('.receipt')[document.querySelectorAll('.receipt').length - 1].classList.remove("hidden");

  if (data) {
    let response_speed = randint(1000, 2000);
    let typing_speed = (data.length * 10);

    setTimeout(function(){
      document.getElementsByClassName("typing-indicator")[0].classList.remove("hidden");

      setTimeout(function(){
        update_transcript('BOT: ', data, new Date());
        document.getElementsByClassName("typing-indicator")[0].classList.add("hidden");
        document.getElementsByClassName("error")[0].classList.add("hidden");
      }, typing_speed);
    }, response_speed);
  }
});

验证效果

  • 移除反向布局后,消息按正常顺序排列,避免iOS Safari的滚动计算异常。
  • requestAnimationFrame确保DOM渲染完成后再滚动,适配iOS的渲染机制,保证滚动操作稳定生效。

内容的提问来源于stack exchange,提问作者tony

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 15:01:32