【1024快乐】用HTML写一个本地版心愿墙页面

AI时代,不改热爱,1024快乐。


一、效果展示

这个网页在本地实现了心愿墙功能。

页面效果如图。

二、开发说明

1.借助AI大模型开发,但是我阅读检查并测试了代码,在本地Edge浏览器下运行流畅。

  1. HTML代码和CSS、JS代码都放在同一个文件里,但分区明确。文件下载后即可双击运行。

3.可以增加和删除心愿。目前未设置批量删除功能,也不能修改已发送的心愿。

4.已发布的心愿使用localStorage 进行本地存储,刷新页面后也不丢失。信息不联网不上传。

5.采用响应式设计,兼顾网页端与移动端体验。

6.遵循网页开发最佳实践,遵循无障碍设计特性。

7.CSS设计令牌集中配置,便于后续扩展。

三、完整代码

html 复制代码
<!DOCTYPE html>
<!--
  ============================================================================
  文件名称:wish-wall.html
  版本     :v2.0
  更新记录 :
    v2.0 - 修复:输入框占位符显示异常(移除特殊符号,改为纯文本)
         - 修复:页面标题「心愿墙」后的句号装饰移除
         - 新增:署名功能(独立的署名输入框,随消息一同展示)
         - 新增:删除单条消息功能(每条消息附删除按钮)
         - 重构:JS 升级为「内存状态数组 + 统一渲染」的数据驱动架构
  项目描述:心愿寄语墙 ------ 用户输入祝福/愿望(可署名),发送后显示在展示区域,
            支持单条删除与 localStorage 持久化。
  技术栈  :纯 HTML5 + CSS3 + 原生 JavaScript(无外部依赖)
  ============================================================================
-->
<html lang="zh-CN">
<head>
  <!-- ========== 基础元信息 ========== -->
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta name="description" content="写下你的心愿与祝福,发送到心愿墙上。" />
  <meta name="author" content="Your Name" />
  <title>心愿墙 · 写下你的祝福</title>

  <!-- ========== 页面样式(CSS) ========== -->
  <style>
    /* ----------------------------------------------------------------------
       设计令牌(Design Tokens)------所有变量集中于此,换主题零业务改动
       ---------------------------------------------------------------------- */
    :root {
      /* ------ 颜色 ------ */
      --color-bg:            #f4f6fb;
      --color-surface:       #ffffff;
      --color-primary:       #6c5ce7;
      --color-primary-hover: #5a4bd1;
      --color-primary-active:#4a3cb8;
      --color-danger:        #e17055;            /* v2.0 新增:删除操作的警示色 */
      --color-danger-hover:  #c94f35;
      --color-text:          #2d3436;
      --color-text-muted:    #8a94a6;
      --color-border:        #e3e8f0;
      --color-focus-ring:    rgba(108, 92, 231, .35);

      /* ------ 尺寸 ------ */
      --radius-sm: 8px;
      --radius-md: 14px;
      --radius-lg: 20px;
      --space-xs:  6px;
      --space-sm:  12px;
      --space-md:  20px;
      --space-lg:  32px;

      /* ------ 展示区高度可调的范围 ------ */
      --display-min-h: 180px;
      --display-max-h: 480px;

      /* ------ 动效 / 阴影 ------ */
      --transition-fast: 160ms ease;
      --transition-pop:  320ms cubic-bezier(.2, .9, .3, 1.3);
      --shadow-card: 0 8px 30px rgba(44, 62, 80, .08);
      --shadow-btn:  0 4px 14px rgba(108, 92, 231, .35);
    }

    /* 1. 现代盒模型 + 平滑滚动 */
    *, *::before, *::after { box-sizing: border-box; }
    html { scroll-behavior: smooth; }

    body {
      margin: 0;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue",
                   Arial, sans-serif;
      font-size: 16px;
      line-height: 1.6;
      color: var(--color-text);
      background:
        radial-gradient(circle at 15% 20%, rgba(108,92,231,.10), transparent 40%),
        radial-gradient(circle at 85% 80%, rgba(0,184,148,.10), transparent 40%),
        var(--color-bg);
    }

    /* 2. 页头 */
    .site-header {
      text-align: center;
      padding: var(--space-lg) var(--space-md) var(--space-sm);
    }
    .site-header h1 {
      margin: 0;
      font-size: clamp(1.6rem, 4vw, 2.2rem);
      font-weight: 700;
      letter-spacing: .05em;
    }
    .site-header p {
      margin: var(--space-xs) 0 0;
      color: var(--color-text-muted);
      font-size: .95rem;
    }

    /* 3. 主容器 */
    .main {
      width: 100%;
      max-width: 680px;
      margin: 0 auto;
      padding: 0 var(--space-md) var(--space-lg);
      display: flex;
      flex-direction: column;
      gap: var(--space-lg);
    }

    /* 4. 通用卡片 */
    .card {
      background: var(--color-surface);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-lg);
      box-shadow: var(--shadow-card);
      padding: var(--space-lg);
    }

    /* 5. 展示区(可垂直拖大/拖小,范围受限) */
    .display-area {
      min-height: var(--display-min-h);
      max-height: var(--display-max-h);
      overflow-y: auto;
      resize: vertical;
      display: flex;
      flex-direction: column;
      gap: var(--space-sm);
      scrollbar-width: thin;
      scrollbar-color: var(--color-border) transparent;
    }
    .display-area::-webkit-scrollbar { width: 8px; }
    .display-area::-webkit-scrollbar-thumb {
      background: var(--color-border);
      border-radius: 4px;
    }

    .display-area__title {
      margin: 0 0 var(--space-sm);
      font-size: .85rem;
      font-weight: 600;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: var(--color-text-muted);
      display: flex;
      align-items: center;
      gap: var(--space-xs);
    }
    .display-area__title::after {
      content: "";
      flex: 1;
      height: 1px;
      background: var(--color-border);
    }

    /* ------ 空状态占位 ------ */
    .empty-state {
      margin: auto;
      text-align: center;
      color: var(--color-text-muted);
      font-size: .95rem;
      user-select: none;
    }
    .empty-state .emoji { display: block; font-size: 2rem; margin-bottom: var(--space-xs); }

    /* ----------------------------------------------------------------------
       ------ 单条消息(v2.0:改为相对定位,为右上角删除按钮让位)------
       ---------------------------------------------------------------------- */
    .message {
      position: relative;                         /* 删除按钮的绝对定位基准 */
      background: linear-gradient(135deg, rgba(108,92,231,.08), rgba(0,184,148,.06));
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      padding: var(--space-sm) var(--space-md);
      padding-right: 40px;                        /* 右侧留出删除按钮空间,防文字重叠 */

      /* 进场初始态(.message--enter 触发过渡);删除动画见 .message--leave */
      opacity: 0;
      transform: translateY(8px) scale(.98);
    }
    .message--enter {
      opacity: 1;
      transform: none;
      transition: opacity var(--transition-pop), transform var(--transition-pop);
    }
    /* 离场动画:JS 先加此类,待 transitionend 后真正移除节点 */
    .message--leave {
      opacity: 0;
      transform: translateX(24px);
      transition: opacity 200ms ease, transform 200ms ease;
    }

    .message__text {
      margin: 0;
      overflow-wrap: break-word;                  /* 长文本自动换行,防撑破布局 */
      word-break: break-word;
    }
    .message__meta {
      margin: var(--space-xs) 0 0;
      font-size: .75rem;
      color: var(--color-text-muted);
    }
    /* 署名与时间同一行:署名在左,时间在右 */
    .message__meta .meta__name { font-weight: 600; color: var(--color-primary); }
    .message__meta .meta__time { float: right; }  /* 时间靠右 */

    /* ----------------------------------------------------------------------
       ------ 删除按钮(v2.0 新增)------
       使用真实的 <button> 而非 <span onclick>:天然支持键盘操作与语义。
       ---------------------------------------------------------------------- */
    .message__delete {
      position: absolute;
      top: var(--space-xs);
      right: var(--space-xs);
      width: 28px;
      height: 28px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      font-size: 1rem;
      line-height: 1;
      color: var(--color-text-muted);
      background: transparent;
      border: none;
      border-radius: var(--radius-sm);
      cursor: pointer;
      transition: color var(--transition-fast),
                  background-color var(--transition-fast);
    }
    /* 默认弱化显示,整条消息悬停时才显现(触屏设备则常显,见媒体查询) */
    .message:not(:hover) .message__delete:not(:focus-visible) { opacity: .45; }
    .message__delete:hover { color: #fff; background: var(--color-danger); }
    .message__delete:focus-visible {
      outline: none;
      box-shadow: 0 0 0 3px rgba(225, 112, 85, .35);
    }
    .message__delete:active { background: var(--color-danger-hover); }

    /* 6. 输入区 */
    .input-form { display: flex; flex-direction: column; gap: var(--space-md); }

    .field { display: flex; flex-direction: column; gap: var(--space-xs); }
    .field__label { font-size: .9rem; font-weight: 600; }

    /* 文本域 */
    .field__textarea {
      width: 100%;
      min-height: 110px;
      max-height: 260px;
      resize: vertical;
      padding: var(--space-sm) var(--space-md);
      font: inherit;
      color: inherit;
      background: #fbfcfe;
      border: 1.5px solid var(--color-border);
      border-radius: var(--radius-sm);
      transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
    }
    .field__textarea::placeholder { color: var(--color-text-muted); }
    .field__textarea:hover:not(:disabled)  { border-color: var(--color-primary); }
    .field__textarea:focus {
      outline: none;
      border-color: var(--color-primary);
      box-shadow: 0 0 0 4px var(--color-focus-ring);
    }
    .field__textarea:disabled { opacity: .6; cursor: not-allowed; }

    /* ----------------------------------------------------------------------
       ------ 单行输入框(v2.0 新增:署名)------
       样式与 textarea 保持一致,抽公共属性会让示例冗长,这里适度重复。
       ---------------------------------------------------------------------- */
    .field__input {
      width: 100%;
      padding: 10px var(--space-md);
      font: inherit;
      color: inherit;
      background: #fbfcfe;
      border: 1.5px solid var(--color-border);
      border-radius: var(--radius-sm);
      transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
    }
    .field__input::placeholder { color: var(--color-text-muted); }
    .field__input:hover:not(:disabled) { border-color: var(--color-primary); }
    .field__input:focus {
      outline: none;
      border-color: var(--color-primary);
      box-shadow: 0 0 0 4px var(--color-focus-ring);
    }

    .field__hint {
      font-size: .78rem;
      color: var(--color-text-muted);
      text-align: right;
    }

    /* 7. 发送按钮 */
    .btn-send {
      align-self: flex-end;
      display: inline-flex;
      align-items: center;
      gap: var(--space-xs);
      padding: 12px 36px;
      font: inherit;
      font-weight: 600;
      letter-spacing: .08em;
      color: #fff;
      background: linear-gradient(135deg, var(--color-primary), #8e7cf3);
      border: none;
      border-radius: var(--radius-md);
      box-shadow: var(--shadow-btn);
      cursor: pointer;
      transition: transform var(--transition-fast),
                  box-shadow  var(--transition-fast),
                  background  var(--transition-fast);
    }
    .btn-send:hover:not(:disabled) {
      background: linear-gradient(135deg, var(--color-primary-hover), var(--color-primary));
      transform: translateY(-2px);
      box-shadow: 0 8px 20px rgba(108, 92, 231, .45);
    }
    .btn-send:active:not(:disabled) {
      transform: translateY(0);
      background: var(--color-primary-active);
    }
    .btn-send:focus-visible {
      outline: none;
      box-shadow: 0 0 0 4px var(--color-focus-ring);
    }
    .btn-send:disabled { opacity: .55; cursor: not-allowed; box-shadow: none; }
    .btn-send__icon { width: 1.1em; height: 1.1em; }

    /* 8. 页脚 */
    .site-footer {
      margin-top: auto;
      padding: var(--space-md);
      text-align: center;
      font-size: .8rem;
      color: var(--color-text-muted);
    }

    /* 9. 尊重"减少动态效果"系统偏好 */
    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
      }
    }

    /* 10. 小屏适配 */
    @media (max-width: 480px) {
      .card { padding: var(--space-md); }
      .btn-send { align-self: stretch; justify-content: center; }
      /* 触屏无悬停态,删除按钮常显,避免"找不到删除入口" */
      .message:not(:hover) .message__delete:not(:focus-visible) { opacity: 1; }
    }
  </style>
</head>

<body>
  <!-- ==================== 页头(v2.0:标题句号已移除)==================== -->
  <header class="site-header">
    <h1>心愿墙</h1>
    <p>把此刻的心意写下来,让它留在这里。</p>
  </header>

  <!-- ==================== 主体 ==================== -->
  <main class="main">

    <!-- ------------ 展示区 ------------ -->
    <section class="card display-area" id="displayArea"
             role="log" aria-live="polite" aria-label="已发送的心愿列表"
             tabindex="0">
      <h2 class="display-area__title">大家的心愿</h2>
      <div class="empty-state" id="emptyState">
        <span class="emoji" aria-hidden="true">💌</span>
        还没有人留下心愿,来做第一个吧~
      </div>
      <!-- 消息列表:由 JS 根据数据统一渲染到此(id 便于 JS 定位) -->
      <div class="display-area__list" id="messageList"></div>
    </section>

    <!-- ------------ 输入区 ------------ -->
    <section class="card">
      <form class="input-form" id="wishForm" novalidate>

        <!-- v2.0 新增:署名输入(选填)-->
        <div class="field">
          <label class="field__label" for="nameInput">署名</label>
          <input class="field__input" type="text"
                 id="nameInput" name="name"
                 maxlength="20"
                 placeholder="留空则显示为「匿名」"
                 autocomplete="nickname" />           <!-- 浏览器可提示已存昵称 -->
        </div>

        <div class="field">
          <label class="field__label" for="wishInput">写下你的心愿或祝福</label>
          <!-- v2.0 修复:占位符改为纯文本,移除可能引发显示异常的特殊符号 -->
          <textarea class="field__textarea"
                    id="wishInput"
                    name="wish"
                    maxlength="200"
                    placeholder="例如:平安健康,开心快乐!"
                    aria-describedby="charCounter"></textarea>
          <p class="field__hint" id="charCounter" aria-live="polite">0 / 200</p>
        </div>

        <button class="btn-send" type="submit" id="sendBtn">
          <svg class="btn-send__icon" viewBox="0 0 24 24" fill="none"
               stroke="currentColor" stroke-width="2"
               stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <line x1="22" y1="2" x2="11" y2="13"></line>
            <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
          </svg>
          发送
        </button>
      </form>
    </section>
  </main>

  <!-- ==================== 页脚 ==================== -->
  <footer class="site-footer">
    <p>愿每一个心愿都被温柔以待 · Made with ❤️</p>
  </footer>

  <!-- ==================== 业务逻辑(JavaScript) ====================
       v2.0 架构说明(数据驱动):
         state.messages 是唯一事实来源(Single Source of Truth)
         所有操作------新增、删除、初始化------都只修改 state,
         再调用 render() 统一渲染,DOM 永远与数据保持一致。
       模块划分:
         ① CONFIG      ------ 常量配置
         ② state       ------ 内存状态
         ③ 持久化       ------ load/save(容错处理)
         ④ DOM 构建     ------ 纯函数:数据 → 节点(textContent 防 XSS)
         ⑤ 渲染        ------ render() / 进场离场动画
         ⑥ 业务操作     ------ addMessage / deleteMessage
         ⑦ 事件绑定     ------ init()
  -->
  <script>
    (function () {
      "use strict";

      /* ---------------------------------------------------------------
       * ① 常量配置
       * ------------------------------------------------------------- */
      const CONFIG = Object.freeze({
        MAX_LENGTH: 200,              // 心愿正文上限(与 HTML maxlength 一致)
        MAX_NAME_LENGTH: 20,          // v2.0:署名上限
        STORAGE_KEY: "wish-wall:v2",  // v2.0:数据结构变更,键版本号升级为 v2
        ANONYMOUS_NAME: "匿名",       // 未填写署名时的默认显示
        SHOW_TIMESTAMP: true
      });

      /* ---------------------------------------------------------------
       * ② 元素引用缓存
       * ------------------------------------------------------------- */
      const els = {
        displayArea: document.getElementById("displayArea"),
        emptyState:  document.getElementById("emptyState"),
        messageList: document.getElementById("messageList"), // v2.0:消息挂载点
        form:        document.getElementById("wishForm"),
        input:       document.getElementById("wishInput"),
        nameInput:   document.getElementById("nameInput"),   // v2.0
        charCounter: document.getElementById("charCounter"),
        sendBtn:     document.getElementById("sendBtn")
      };

      /* ---------------------------------------------------------------
       * ③ 内存状态(唯一事实来源)
       * 消息格式:{ id: string, text: string, name: string, time: number }
       * id 用于精确删除------渲染时写入 data-id,删除时据此回查。
       * ------------------------------------------------------------- */
      const state = {
        messages: []
      };

      /* ---------------------------------------------------------------
       * 工具:生成唯一 ID
       * crypto.randomUUID 为主,旧浏览器回退到时间戳+随机数方案。
       * ------------------------------------------------------------- */
      function generateId() {
        if (window.crypto && typeof window.crypto.randomUUID === "function") {
          return window.crypto.randomUUID();
        }
        return "id-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
      }

      /* ---------------------------------------------------------------
       * ④ 持久化(容错:数据损坏时静默降级,绝不让整页脚本崩溃)
       * ------------------------------------------------------------- */
      function loadHistory() {
        try {
          const raw = localStorage.getItem(CONFIG.STORAGE_KEY);
          const list = raw ? JSON.parse(raw) : [];
          if (!Array.isArray(list)) return [];
          // 逐条校验字段,丢弃损坏条目(防御外部篡改存储)
          return list.filter(
            (m) => m && typeof m.text === "string" && typeof m.time === "number"
          );
        } catch {
          return [];
        }
      }
      function persist() {
        try {
          localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(state.messages));
        } catch { /* 隐私模式等场景写入失败,忽略即可 */ }
      }

      /* ---------------------------------------------------------------
       * ⑤ 纯函数:根据一条数据创建 DOM(数据 → 节点,无副作用、可单测)
       * ⚠️ 安全要点:用户输入一律 textContent 写入,杜绝 XSS。
       * ------------------------------------------------------------- */
      function createMessageElement({ id, text, name, time }) {
        const item = document.createElement("article");
        item.className = "message";
        item.dataset.id = id;                       // 删除时定位用

        // ------ 删除按钮(v2.0)------
        // 真实 <button> + aria-label:语义正确、键盘可达;
        // 符号 × 被 aria-hidden 隐藏,避免屏幕阅读器读作"乘号"。
        const btn = document.createElement("button");
        btn.type = "button";                        // 防止意外触发表单提交
        btn.className = "message__delete";
        btn.setAttribute("aria-label", "删除这条心愿");
        btn.dataset.action = "delete";              // 事件委托时的识别标记
        const btnIcon = document.createElement("span");
        btnIcon.setAttribute("aria-hidden", "true");
        btnIcon.textContent = "×";
        btn.appendChild(btnIcon);

        // ------ 正文 ------
        const p = document.createElement("p");
        p.className = "message__text";
        p.textContent = text;

        // ------ 署名 + 时间(v2.0:署名展示)------
        const meta = document.createElement("p");
        meta.className = "message__meta";
        const nameSpan = document.createElement("span");
        nameSpan.className = "meta__name";
        nameSpan.textContent = name || CONFIG.ANONYMOUS_NAME;
        meta.appendChild(nameSpan);
        if (CONFIG.SHOW_TIMESTAMP) {
          const timeSpan = document.createElement("span");
          timeSpan.className = "meta__time";
          timeSpan.textContent = new Date(time).toLocaleTimeString("zh-CN", {
            hour: "2-digit", minute: "2-digit"
          });
          meta.appendChild(timeSpan);
        }

        item.append(btn, p, meta);
        return item;
      }

      /* ---------------------------------------------------------------
       * ⑥ 统一渲染:DOM 完全由 state 重建(清空 → 重建 → 动画进场)
       * 消息量不大(本地场景),全量重建简单可靠;
       * 若日后数据量大,可改为增量 diff,对外接口不变。
       * ------------------------------------------------------------- */
      function render() {
        // 空状态显隐
        els.emptyState.hidden = state.messages.length > 0;

        // 清空列表
        els.messageList.replaceChildren();

        // 文档片段(DocumentFragment):批量节点一次性插入,只触发一次重排
        const fragment = document.createDocumentFragment();
        state.messages.forEach((msg) => {
          fragment.appendChild(createMessageElement(msg));
        });
        els.messageList.appendChild(fragment);

        // 进场动画:插入后统一在下一帧加状态类
        requestAnimationFrame(() => {
          els.messageList.querySelectorAll(".message").forEach((el) => {
            el.classList.add("message--enter");
          });
        });

        // 滚动到最新
        els.displayArea.scrollTop = els.displayArea.scrollHeight;
      }

      /* ---------------------------------------------------------------
       * ⑦ 业务操作:新增
       * ------------------------------------------------------------- */
      function addMessage(text, name) {
        const msg = {
          id: generateId(),
          text,
          name: name || "",                        // 存空串,渲染时回退为「匿名」
          time: Date.now()
        };
        state.messages.push(msg);                  // 1. 先改数据
        render();                                  // 2. 再统一渲染
        persist();                                 // 3. 最后持久化
      }

      /* ---------------------------------------------------------------
       * ⑧ 业务操作:删除(v2.0)
       * 流程:加离场动画 → 动画结束后从数据移除并重渲染 → 持久化
       * 防重复点击:动画期间对按钮临时禁用。
       * ------------------------------------------------------------- */
      function deleteMessage(id, element) {
        const btn = element.querySelector(".message__delete");
        if (btn) btn.disabled = true;

        element.classList.remove("message--enter");
        element.classList.add("message--leave");   // 触发离场过渡

        // transitionend 后执行真正移除;{ once: true } 自动解绑,防止内存泄漏
        element.addEventListener("transitionend", function onEnd() {
          state.messages = state.messages.filter((m) => m.id !== id);
          render();
          persist();
        }, { once: true });
      }

      /* ---------------------------------------------------------------
       * ⑨ 事件处理
       * ------------------------------------------------------------- */
      function handleSubmit(event) {
        event.preventDefault();

        const text = els.input.value.trim();
        const name = els.nameInput.value.trim().slice(0, CONFIG.MAX_NAME_LENGTH);
        if (!text) { els.input.focus(); return; }
        if (text.length > CONFIG.MAX_LENGTH) { return; } // 双保险校验

        els.sendBtn.disabled = true;
        addMessage(text, name);
        els.input.value = "";
        els.nameInput.value = "";
        updateCounter();
        els.sendBtn.disabled = false;
        els.input.focus();
      }

      function updateCounter() {
        els.charCounter.textContent = `${els.input.value.length} / ${CONFIG.MAX_LENGTH}`;
      }

      /* ---------------------------------------------------------------
       * 事件委托(v2.0 删除功能的推荐实现):
       * 不在每条消息上单独绑监听,而是在父容器上绑一个,
       * 利用事件冒泡统一处理 ------ 动态增删消息后无需重新绑定,
       * 监听器数量恒定,避免内存泄漏风险。
       * ------------------------------------------------------------- */
      function handleListClick(event) {
        const btn = event.target.closest("button[data-action='delete']");
        if (!btn) return;
        const item = btn.closest(".message");
        if (!item) return;
        deleteMessage(item.dataset.id, item);
      }

      /* ---------------------------------------------------------------
       * 入口
       * ------------------------------------------------------------- */
      function init() {
        els.form.addEventListener("submit", handleSubmit);
        els.input.addEventListener("input", updateCounter);
        els.messageList.addEventListener("click", handleListClick); // 事件委托

        // Ctrl/Cmd + Enter 快捷发送
        els.input.addEventListener("keydown", (e) => {
          if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
            els.form.requestSubmit();
          }
        });

        // 恢复历史数据
        state.messages = loadHistory();
        render();
      }

      init();
    })();
  </script>
</body>
</html>
相关推荐
wshzd1 小时前
LLM之Agent(117)|DeepSeek-Harness(二十四)Host/Client 分离与 Typert RPC 生成
1024程序员节
长不大的杨瑞杰1 小时前
OpenCode v2 CI 与自动化实战:无交互运行、权限收口与制品闭环
运维·人工智能·ci/cd·自动化·软件工程·1024程序员节
limit00751 小时前
三维场景五段链路的资源与依赖排查
1024程序员节
骑着蜗牛撵大象3271 小时前
N1 小盒子撑起 AI 接口中转层:New-API 接管本地 Ollama,用 Token 分发模型能力
docker·llama·1024程序员节·api网关·飞牛nas·new-api
java1234_小锋1 小时前
【技术专题】CSS3 网页技术 - CSS3 简介 & CSS 语法:选择器、属性、值、声明块
css3·1024程序员节
前端 贾公子1 小时前
LangGraph == 顺序结构
1024程序员节
思码梁田1 小时前
CSS 浏览器厂商前缀:从「写 5 遍」到现代自动化兼容
前端·css·css3·border-radius·1024程序员节·box-shadow·厂商前缀
羞儿1 小时前
【DSH】创造模式与内核边界:一切皆插件的递归与自证
agent·1024程序员节·harness
hsg771 小时前
简述:基于Ubuntu24编译发布rust开发的html+web后端服务的方法
rust·1024程序员节