HTML 块脚本可以调用 JS 片段中的函数吗

本贴最后更新于 205 天前,其中的信息可能已经天翻地覆

我想在列表中创建多个计时器,效果如下

timer1.png

html 块脚本如下

<div>
<style>
  .scoped-timer {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    margin: 8px 0;
    background: #f0f8ff;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    font-family: sans-serif;
    font-size: 14px;
    color: #333;
  }
  .scoped-timer .timer-controls {
    display: flex;
    gap: 8px;
    margin-bottom: 6px;
  }
  .scoped-timer button {
    padding: 6px 12px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-weight: bold;
  }
  .scoped-timer .start-btn {
    background-color: #28a745; /* Green */
    color: white;
  }
  .scoped-timer .stop-btn {
    background-color: #dc3545; /* Red */
    color: white;
  }
  .scoped-timer button:disabled {
    background-color: #ccc;
    cursor: not-allowed;
  }
  .scoped-timer .row {
    display: flex;
    gap: 20px;
    margin-top: 4px;
  }
</style>
<div class="scoped-timer">
  <div class="timer-controls">
    <button class="start-btn" onclick="handleStart(this)">开始</button>
    <button class="stop-btn" onclick="handleStop(this)">停止</button>
  </div>
  <div class="row">
    <div>开始时间:<span class="start-time">--</span></div>
    <div>停止时间:<span class="stop-time">--</span></div>
  </div>
  <div class="row">
    <div>本次时长(分钟):<span class="current-duration">0.00</span></div>
    <div>累计时长(分钟):<span class="total-duration">0.00</span></div>
  </div>
</div>
</div>

并且已经开启了** 设置 - 编辑器 - 允许执行 HTML 块内脚本**

HTML 块内脚本主要就是两个按钮,如下

<div class="timer-controls">
    <button class="start-btn" onclick="handleStart(this)">开始</button>
    <button class="stop-btn" onclick="handleStop(this)">停止</button>
  </div>

分别调用了两个函数 handleStart(this)handleStop(this)。而这两个函数我是写在自定义的代码片段,JS 中,如下

(() => {
  window.timerStates = window.timerStates || new Map();
  function handleStart(button) {
    const container = button.closest('.scoped-timer');
    const stopBtn = container.querySelector(".stop-btn");
    const startTimeSpan = container.querySelector(".start-time");
    const currentSpan = container.querySelector(".current-duration");

    const containerId = container.closest('[data-type="NodeList"]').dataset.nodeId + '_' + Array.from(container.closest('[data-type="NodeList"]').children).indexOf(container.closest('.li'));

    if (!timerStates.has(containerId)) {

      const start = Date.now();
      if (startTimeSpan.textContent === "--") {
        startTimeSpan.textContent = new Date().toLocaleString();
      }
      const state = {
        startTime: start,
        elapsed: 0,
        interval: setInterval(() => {
          const now = (Date.now() - state.startTime) / 60000 + state.elapsed;
          currentSpan.textContent = now.toFixed(2);
        }, 60000)
      };
      timerStates.set(containerId, state);
      button.textContent = "暂停";
    } else {
      const state = timerStates.get(containerId);
      state.elapsed += (Date.now() - state.startTime) / 60000;
      clearInterval(state.interval);
      timerStates.delete(containerId);
      button.textContent = "开始";
    }
  }

  function handleStop(button) {
    const container = button.closest('.scoped-timer');
    const startBtn = container.querySelector(".start-btn");
    const stopTimeSpan = container.querySelector(".stop-time");
    const currentSpan = container.querySelector(".current-duration");
    const totalSpan = container.querySelector(".total-duration");

    const containerId = container.closest('[data-type="NodeList"]').dataset.nodeId + '_' + Array.from(container.closest('[data-type="NodeList"]').children).indexOf(container.closest('.li'));

    const state = timerStates.get(containerId);
    if (state) {
      state.elapsed += (Date.now() - state.startTime) / 60000;
      clearInterval(state.interval);
      currentSpan.textContent = state.elapsed.toFixed(2);
      timerStates.delete(containerId);
    }

    stopTimeSpan.textContent = new Date().toLocaleString();
    startBtn.disabled = true;
    button.disabled = true;

    const listRoot = container.closest('[data-type="NodeList"]');
    let total = 0;
    listRoot.querySelectorAll('.current-duration').forEach(span => {
      total += parseFloat(span.textContent || '0');
    });
    totalSpan.textContent = total.toFixed(2);
  }
})();

handleStart(this)handleStop(this) 这两个函数是存在的,且 JS 片段也打开了,为什么我在文档中点击按钮,报错说方法不存在

timer2.png

为什么会这样呢?我可以怎么修改,使计时器生效


我这个定时器最后效果如图:

  1. 在页面没有重新加载的时候,是可以记录任务花费的时间,任务的开始时间,本次时长,结束时间和累计时长都记录在计时器下面的一个列表项中。
    timer4.png
  2. 但是一旦页面重新加载,那么最后一个记录任务的开始时间,本次时长,结束时间和累计时长的列表项的值消失了,如图
    timer5.png
  3. 我想页面重新加载,以前的记录不会消失,不知道该怎么做,我也不知道为什么一个列表项内容消失了,一个没有。代码都是一样的

新的定时器代码如下

html 模块代码如下

<div>
<style>
  .scoped-timer {
    padding: 0;
    border: none;
    background: none;
    font-family: sans-serif;
    font-size: 14px;
    color: #333;
  }
  .scoped-timer .timer-controls {
    display: flex;
    gap: 45px;
    margin-bottom: 6px;
  }
  .scoped-timer button {
    padding: 6px 12px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-weight: bold;
  }
  .scoped-timer .start-btn {
    background-color: #28a745; /* Green */
    color: white;
  }
  .scoped-timer .stop-btn {
    background-color: #dc3545; /* Red */
    color: white;
  }
  .scoped-timer button:disabled {
    background-color: #ccc;
    cursor: not-allowed;
  }
</style>
<div class="scoped-timer">
  <div class="timer-controls">
    <button class="start-btn" onclick="handleStart(this)">开始</button>
    <button class="stop-btn" onclick="handleStop(this)">停止</button>
  </div>
</div>
</div>

新的 JS 代码片段如下,


  window.timerStates = window.timerStates || new Map();

  function handleStart(button) {
    const container = button.closest('.scoped-timer');
    const host = container.getRootNode()?.host;
    // const containerId = host?.closest('[data-type="NodeList"]').dataset.nodeId + '_' + Array.from(host?.closest('[data-type="NodeList"]').children).indexOf(host?.closest('.li'));
    // console.log(containerId);
    const listItem = host?.closest('.li');
    const listNode = host?.closest('[data-type="NodeList"]');
    const nodeId = listNode.dataset.nodeId;
    const siblings = Array.from(listNode.querySelectorAll('.li'));
    const index = siblings.indexOf(listItem);
    const dataItem = siblings[index + 1];
    const dataContent = dataItem?.querySelector('[data-type="NodeParagraph"] > div[contenteditable]');
    const containerId = nodeId + '_' + index;
    console.log(containerId);

    if (!window.timerStates.has(containerId)) {
      const startTime = Date.now();
      const state = {
        startTime,
        elapsed: 0,
        interval: setInterval(() => {
          const minutes = ((Date.now() - state.startTime) / 60000 + state.elapsed).toFixed(1);
          if (dataContent) updateDataContent(dataContent, null, minutes, null);
        }, 60000)
      };
      if (dataContent) updateDataContent(dataContent, new Date().toLocaleString());
      window.timerStates.set(containerId, state);
      button.textContent = '暂停';
    } else {
      const state = window.timerStates.get(containerId);
      state.elapsed += (Date.now() - state.startTime) / 60000;
      clearInterval(state.interval);
      window.timerStates.delete(containerId);
      button.textContent = '开始';
    }
  }

  function handleStop(button) {
    const container = button.closest('.scoped-timer');
    const host = container.getRootNode()?.host;
    // const containerId = host?.closest('[data-type="NodeList"]').dataset.nodeId + '_' + Array.from(host?.closest('[data-type="NodeList"]').children).indexOf(host?.closest('.li'));
    // console.log(containerId);
    const startBtn = container.querySelector(".start-btn");
    const listItem = host?.closest('.li');
    const listNode = host?.closest('[data-type="NodeList"]');
    const nodeId = listNode.dataset.nodeId;
    const siblings = Array.from(listNode.querySelectorAll('.li'));
    const index = siblings.indexOf(listItem);
    const dataItem = siblings[index + 1];
    const dataContent = dataItem?.querySelector('[data-type="NodeParagraph"] > div[contenteditable]');
    const containerId = nodeId + '_' + index;
    console.log(containerId);
    const state = window.timerStates.get(containerId);

    let current = 0;
    if (state) {
      console.log("清除计时器");
      current = state.elapsed + (Date.now() - state.startTime) / 60000;
      clearInterval(state.interval);
      window.timerStates.delete(containerId);
    }

    if (dataContent) {
      let total = current;
      for (let i = 0; i < index; i++) {
        const contentDiv = siblings[i + 1]?.querySelector('[data-type="NodeParagraph"] > div[contenteditable]');
        if (contentDiv) {
          const match = contentDiv.textContent.match(/累计时长(分钟): (\d+(\.\d+)?)/);
          if (match) total += parseFloat(match[1]);
        }
      }
      updateDataContent(dataContent, null, current.toFixed(1), new Date().toLocaleString(), total.toFixed(1));
    }

    startBtn.disabled = true;
    button.disabled = true;
  }

  function updateDataContent(div, start = null, current = null, stop = null, total = null) {
    let content = div.innerText || "";
    // console.log(content);
    function escapeRegExp(string) {
      return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    function replaceOrAppend(label, value) {
      const escapedLabel = escapeRegExp(label); // "用户\\(名称\\)"
      const regex = new RegExp(`${escapedLabel}: [^\s\n]*`);
      const str = `${label}: ${value}`;
      if (regex.test(content)) {
        content = content.replace(regex, str);
      } else {
        // content += `  ${str}`;
        // 检查字符串是否以"停止时间"开头
        if (str.startsWith("停止时间")) {
          // 添加shift+回车(用换行符表示)后接str
          content += '\n' + str;
        } else {
          // 添加空格后接str
          content += `  ${str}`;
        }
      }
    }

    if (start !== null) replaceOrAppend("开始时间", start);
    if (current !== null) replaceOrAppend("本次时长(分钟)", current);
    if (stop !== null) replaceOrAppend("停止时间", stop);
    if (total !== null) replaceOrAppend("累计时长(分钟)", total);

    div.innerText = content.trim();
  }
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28444 引用 • 119764 回帖
  • Q&A

    提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。

    11154 引用 • 50648 回帖 • 52 关注
2 操作
amykiki 在 2025-05-30 21:22:02 更新了该帖
amykiki 在 2025-05-30 21:19:55 更新了该帖

相关帖子

被采纳的回答
  • 因为你把函数封装到闭包 (() => {})() 中了,外部是无法读取到闭包中的函数或变量的。

    解决方法,1 去掉闭包 2 把闭包中的函数暴露到外部,比如 window.xxx=xxx

    另外,你代码中的 const containerId = container.closest('') 是无法获取到祖先元素的,因为 container 在 shadow 内部,需要用 const host = container.getRootNode()?.host; 然后 host.closest('')即可。

    下面是我改好的代码,放到 js 代码片段中即可。

    window.timerStates = window.timerStates || new Map();
    function handleStart(button) {
        const container = button.closest('.scoped-timer');
        const stopBtn = container.querySelector(".stop-btn");
        const startTimeSpan = container.querySelector(".start-time");
        const currentSpan = container.querySelector(".current-duration");
        const host = container.getRootNode()?.host;
        const containerId = host?.closest('[data-type="NodeList"]').dataset.nodeId + '_' + Array.from(host?.closest('[data-type="NodeList"]').children).indexOf(host?.closest('.li'));
    
        if (!timerStates.has(containerId)) {
    
            const start = Date.now();
            if (startTimeSpan.textContent === "--") {
                startTimeSpan.textContent = new Date().toLocaleString();
            }
            const state = {
                startTime: start,
                elapsed: 0,
                interval: setInterval(() => {
                    const now = (Date.now() - state.startTime) / 60000 + state.elapsed;
                    currentSpan.textContent = now.toFixed(2);
                }, 60000)
            };
            timerStates.set(containerId, state);
            button.textContent = "暂停";
        } else {
            const state = timerStates.get(containerId);
            state.elapsed += (Date.now() - state.startTime) / 60000;
            clearInterval(state.interval);
            timerStates.delete(containerId);
            button.textContent = "开始";
        }
    }
    
    function handleStop(button) {
        const container = button.closest('.scoped-timer');
        const startBtn = container.querySelector(".start-btn");
        const stopTimeSpan = container.querySelector(".stop-time");
        const currentSpan = container.querySelector(".current-duration");
        const totalSpan = container.querySelector(".total-duration");
        const host = container.getRootNode()?.host;
        const containerId = host?.getRootNode()?.host?.closest('[data-type="NodeList"]').dataset.nodeId + '_' + Array.from(host?.closest('[data-type="NodeList"]').children).indexOf(host?.closest('.li'));
    
        const state = timerStates.get(containerId);
        if (state) {
            state.elapsed += (Date.now() - state.startTime) / 60000;
            clearInterval(state.interval);
            currentSpan.textContent = state.elapsed.toFixed(2);
            timerStates.delete(containerId);
        }
    
        stopTimeSpan.textContent = new Date().toLocaleString();
        startBtn.disabled = true;
        button.disabled = true;
    
        const listRoot = host?.closest('[data-type="NodeList"]');
        let total = 0;
        listRoot.querySelectorAll('protyle-html').forEach(protyleHtml => {
            const span = protyleHtml.shadowRoot.querySelector('.current-duration');
            total += parseFloat(span.textContent || '0');
        });
        totalSpan.textContent = total.toFixed(2);
    }
    

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • daziduan

    @amykiki @wilson 两位先达,帮忙看下。我按 html 块和代码块填充代码后发现不能运行。调试后在控制台发现出错,如下:

    1748948367050.png

    请教如何解决?

  • 其他回帖
  • amykiki

    大佬,新版的代码见问题贴,回复里贴不下了

    1 回复
    1 操作
    amykiki 在 2025-05-30 21:20:29 更新了该回帖
  • wilsons 1

    不清楚你具体的场景是怎样的,这里以计数器为例演示,相信聪明的你看完就明白了。

    html 部分(放到 HTML 代码块中)

    <div>
    <button class="countBtn" style="cursor:pointer;" onclick="addCount(this)">计数</button>
    <div class="count">0</div>
    </div>
    

    js 部分(放到 js 代码片段中)

    // 计数演示程序,仅供参考
    function addCount(button) {
        // 计数
        const countEl = button.nextElementSibling;
        countEl.textContent = parseInt(countEl.textContent)+1;
    
        // 保存修改后的HTML代码块(核心代码)
        const host = button.getRootNode().host;
        const div = host.shadowRoot.querySelector('div');
        host.setAttribute('data-content', Lute.EscapeHTMLStr(div.outerHTML));
      
        // 更新块
        updateBlock(host);
    }
    
    // 此函数同之前的updateBlock完全一样
    async function updateBlock(node) {
        if(!node.matches('[data-node-id][data-type]')) {
            node = node.closest('[data-node-id][data-type]');
        }
        await requestApi('/api/block/updateBlock', {
            "dataType": "dom",
            "data": node.outerHTML,
            "id": node.dataset.nodeId
        });
        async function requestApi(url, data, method = 'POST') {
            return await (await fetch(url, {method: method, body: JSON.stringify(data||{})})).json();
        }
    }
    

    r137.gif

    1 回复
  • amykiki

    大佬,在你的帮助下,我这个自定义列表计时器总算完成了,刚刚试了下,功能一切正常,万分感谢

  • 查看全部回帖

推荐标签 标签

  • Gitea

    Gitea 是一个开源社区驱动的轻量级代码托管解决方案,后端采用 Go 编写,采用 MIT 许可证。

    5 引用 • 16 回帖 • 3 关注
  • OneDrive
    2 引用 • 3 关注
  • Access
    1 引用 • 3 回帖 • 14 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    293 引用 • 4496 回帖 • 688 关注
  • Kubernetes

    Kubernetes 是 Google 开源的一个容器编排引擎,它支持自动化部署、大规模可伸缩、应用容器化管理。

    119 引用 • 54 回帖
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    146 引用 • 442 回帖
  • Caddy

    Caddy 是一款默认自动启用 HTTPS 的 HTTP/2 Web 服务器。

    10 引用 • 54 回帖 • 174 关注
  • 快应用

    快应用 是基于手机硬件平台的新型应用形态;标准是由主流手机厂商组成的快应用联盟联合制定;快应用标准的诞生将在研发接口、能力接入、开发者服务等层面建设标准平台;以平台化的生态模式对个人开发者和企业开发者全品类开放。

    15 引用 • 127 回帖
  • Outlook
    1 引用 • 5 回帖
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    327 引用 • 1395 回帖
  • CodeMirror
    2 引用 • 17 回帖 • 197 关注
  • 电影

    这是一个不能说的秘密。

    125 引用 • 610 回帖
  • GitLab

    GitLab 是利用 Ruby 一个开源的版本管理系统,实现一个自托管的 Git 项目仓库,可通过 Web 界面操作公开或私有项目。

    46 引用 • 72 回帖
  • Notion

    Notion - The all-in-one workspace for your notes, tasks, wikis, and databases.

    10 引用 • 80 回帖 • 1 关注
  • HBase

    HBase 是一个分布式的、面向列的开源数据库,该技术来源于 Fay Chang 所撰写的 Google 论文 “Bigtable:一个结构化数据的分布式存储系统”。就像 Bigtable 利用了 Google 文件系统所提供的分布式数据存储一样,HBase 在 Hadoop 之上提供了类似于 Bigtable 的能力。

    17 引用 • 6 回帖 • 72 关注
  • 前端

    前端技术一般分为前端设计和前端开发,前端设计可以理解为网站的视觉设计,前端开发则是网站的前台代码实现,包括 HTML、CSS 以及 JavaScript 等。

    248 引用 • 1342 回帖
  • 支付宝

    支付宝是全球领先的独立第三方支付平台,致力于为广大用户提供安全快速的电子支付/网上支付/安全支付/手机支付体验,及转账收款/水电煤缴费/信用卡还款/AA 收款等生活服务应用。

    29 引用 • 347 回帖 • 2 关注
  • 知乎

    知乎是网络问答社区,连接各行各业的用户。用户分享着彼此的知识、经验和见解,为中文互联网源源不断地提供多种多样的信息。

    10 引用 • 66 回帖
  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    110 引用 • 153 回帖
  • WebClipper

    Web Clipper 是一款浏览器剪藏扩展,它可以帮助你把网页内容剪藏到本地。

    3 引用 • 9 回帖 • 3 关注
  • GitHub

    GitHub 于 2008 年上线,目前,除了 Git 代码仓库托管及基本的 Web 管理界面以外,还提供了订阅、讨论组、文本渲染、在线文件编辑器、协作图谱(报表)、代码片段分享(Gist)等功能。正因为这些功能所提供的便利,又经过长期的积累,GitHub 的用户活跃度很高,在开源世界里享有深远的声望,并形成了社交化编程文化(Social Coding)。

    213 引用 • 2044 回帖
  • IPFS

    IPFS(InterPlanetary File System,星际文件系统)是永久的、去中心化保存和共享文件的方法,这是一种内容可寻址、版本化、点对点超媒体的分布式协议。请浏览 IPFS 入门笔记了解更多细节。

    20 引用 • 245 回帖 • 249 关注
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 740 关注
  • 职场

    找到自己的位置,萌新烦恼少。

    127 引用 • 1708 回帖 • 1 关注
  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 724 关注
  • V2Ray
    1 引用 • 15 回帖 • 4 关注
  • DevOps

    DevOps(Development 和 Operations 的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。

    59 引用 • 25 回帖 • 5 关注