好的!下面把“评论”组件拆解为最小可运行示例,三份文件 逐行中文注释。功能与截图一致:
localStorage);comments.html(逐行注释)<!DOCTYPE html> <!-- 声明 HTML5 文档类型,启用标准模式 -->
<html lang="zh-CN"> <!-- 页面主语言为中文,有利于无障碍与拼写检查 -->
<head> <!-- 文档头部:放置元信息和样式链接 -->
<meta charset="UTF-8"> <!-- 指定字符编码为 UTF-8,避免中文乱码 -->
<meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 视口设置,移动端友好 -->
<title>评论组件(本地存储)</title> <!-- 浏览器标签标题 -->
<link rel="stylesheet" href="comments.css"> <!-- 引入外部样式文件 -->
</head>
<body> <!-- 页面主体开始 -->
<section class="comment-card"> <!-- 外层卡片容器,控制整体样式与留白 -->
<h2 class="comment-title">评论</h2> <!-- 模块标题 -->
<p id="emptyHint" class="comment-empty">还没有评论,快来抢沙发 ~</p> <!-- 无评论时的提示文案 -->
<ul id="commentList" class="comment-list"></ul> <!-- 评论列表容器(JS 动态渲染 <li>) -->
<form id="commentForm" class="comment-form"> <!-- 表单容器,提交时由 JS 接管 -->
<label class="field"> <!-- 昵称字段 -->
<span class="label">昵称</span> <!-- 字段名 -->
<input id="nick" class="input" type="text" placeholder="你的称呼"> <!-- 昵称输入框 -->
</label>
<label class="field"> <!-- 评论内容字段 -->
<span class="label">评论内容</span> <!-- 字段名 -->
<input id="content" class="input" type="text" placeholder="友善、具体、可被他人理解"> <!-- 评论内容输入框(示意用单行输入) -->
</label>
<div class="actions"> <!-- 按钮区域 -->
<button id="submitBtn" class="btn btn-primary" type="submit">发表</button> <!-- 提交按钮,触发表单提交 -->
<button class="btn" type="reset">重置</button> <!-- 重置按钮,仅清空输入,不清空历史评论 -->
</div>
<p class="note">提示:评论仅保存在本机浏览器(localStorage)。</p> <!-- 说明性文字 -->
</form>
</section>
<script src="comments.js"></script> <!-- 引入交互脚本(读取/写入/渲染评论) -->
</body>
</html>
comments.css(逐行注释)* { box-sizing: border-box; } /* 统一盒模型,padding/border 计入元素尺寸 */
body { margin: 0; padding: 24px; font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans SC",Arial,sans-serif; color:#212529; background:#fff; line-height:1.6; } /* 页面基础排版 */
.comment-card { max-width:1100px; margin:0 auto; padding:20px; border:1px solid #dee2e6; border-radius:12px; background:#fff; } /* 外层卡片样式 */
.comment-title { margin:0 0 12px; font-size:28px; font-weight:700; } /* 标题样式 */
.comment-empty { margin: 4px 0 16px; color:#6c757d; } /* “还没有评论”提示的样式(灰色文字) */
.comment-list { list-style:none; padding:0; margin:0 0 16px; display:grid; gap:12px; } /* 评论列表为网格,项与项留缝 */
.comment-item { padding:12px; background:#f8f9fa; border:1px solid #e9ecef; border-radius:8px; } /* 单条评论的外观 */
.comment-meta { display:flex; gap:8px; align-items:center; font-size:14px; color:#6c757d; margin-bottom:6px; } /* 元信息(昵称/时间) */
.comment-name { font-weight:700; color:#0d6efd; } /* 昵称高亮(蓝色) */
.comment-text { white-space:pre-wrap; word-break:break-word; } /* 评论正文换行与断词策略 */
.comment-form { display:grid; grid-template-columns: 1fr 2fr auto; gap:12px; align-items:end; } /* 表单为 3 列:昵称 / 内容 / 按钮列 */
.field { display:flex; flex-direction:column; gap:6px; } /* 字段容器为纵向:标题 + 输入框 */
.label { font-size:16px; color:#212529; } /* 字段标题样式 */
.input { height:44px; padding:0 12px; border:1px solid #ced4da; border-radius:8px; font-size:16px; outline:none; } /* 输入框基础样式 */
.input:focus { border-color:#0d6efd; box-shadow:0 0 0 2px #0d6efd22; } /* 获得焦点时蓝色描边与微弱高光 */
.actions { display:flex; gap:12px; align-items:center; } /* 按钮组横向排列 */
.btn { height:44px; padding:0 18px; border-radius:8px; border:1px solid #adb5bd; background:#fff; color:#212529; cursor:pointer; } /* 普通按钮外观 */
.btn-primary { border-color:#0d6efd; background:#0d6efd; color:#fff; } /* 主按钮(蓝色) */
.btn:active { transform: translateY(1px); } /* 点击反馈微动效 */
.note { grid-column: 1 / -1; text-align:right; color:#6c757d; margin:6px 0 0; } /* 说明文字占满整行,右对齐,灰色 */
@media (max-width: 768px) {
.comment-form { grid-template-columns:1fr; } /* 小屏改为单列堆叠 */
.actions { justify-content:flex-start; } /* 小屏下按钮靠左 */
.note { text-align:left; } /* 小屏时说明文字靠左 */
}
comments.js(逐行注释)// ===== 1. 常量与工具函数 =====
const STORAGE_KEY = 'comments:' + location.pathname; // 本页评论在 localStorage 的键名(按路径区分不同文章)
/** 读取当前页面的评论数组(不存在则返回空数组) */
function loadComments() { // 定义读取函数
const raw = localStorage.getItem(STORAGE_KEY); // 从 localStorage 读取字符串
try { return raw ? JSON.parse(raw) : []; } // 尝试 JSON 解析,若为空则返回 []
catch { return []; } // 解析失败(损坏)时兜底为空数组
}
/** 将评论数组写回 localStorage */
function saveComments(list) { // 定义保存函数
localStorage.setItem(STORAGE_KEY, JSON.stringify(list)); // 序列化为字符串后写入
}
/** 生成形如 2025-08-25 14:03 的时间字符串 */
function formatTime(d = new Date()) { // 传入日期对象,默认当前时间
const pad = n => String(n).padStart(2, '0'); // 工具:位数不足补 0
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; // 拼接年月日时分
}
// ===== 2. DOM 获取 =====
const listEl = document.getElementById('commentList'); // 评论列表容器
const emptyEl = document.getElementById('emptyHint'); // “还没有评论”提示元素
const formEl = document.getElementById('commentForm'); // 表单元素(用于提交事件)
const nickEl = document.getElementById('nick'); // 昵称输入框
const contentEl= document.getElementById('content'); // 评论内容输入框
const submitBtn= document.getElementById('submitBtn'); // 发表按钮
// ===== 3. 渲染函数 =====
/** 根据数据渲染评论列表 */
function renderComments() { // 定义渲染函数
const data = loadComments(); // 读取全部评论
listEl.innerHTML = ''; // 清空列表容器
emptyEl.style.display = data.length ? 'none' : ''; // 根据是否有数据显示/隐藏“还没有评论”
data.forEach(item => { // 遍历每条评论
const li = document.createElement('li'); // 创建列表项
li.className = 'comment-item'; // 设置样式类名
const meta = document.createElement('div'); // 顶部元信息容器
meta.className = 'comment-meta'; // 添加类名
const name = document.createElement('span'); // 昵称元素
name.className = 'comment-name'; // 昵称样式
name.textContent = item.nick; // 使用 textContent 防止 XSS(仅输出文本)
const time = document.createElement('time'); // 时间元素
time.textContent = item.time; // 写入时间文本
meta.appendChild(name); // 元信息中加入昵称
meta.appendChild(document.createTextNode('·')); // 加一个间隔符号
meta.appendChild(time); // 元信息中加入时间
const text = document.createElement('div'); // 评论正文容器
text.className = 'comment-text'; // 添加样式
text.textContent = item.content; // 使用 textContent 确保仅文本显示(安全)
li.appendChild(meta); // 列表项加入元信息
li.appendChild(text); // 列表项加入正文
listEl.appendChild(li); // 列表中追加该项
});
}
// ===== 4. 提交与校验 =====
formEl.addEventListener('submit', (e) => { // 监听表单提交事件
e.preventDefault(); // 阻止表单默认提交刷新页面
const nick = nickEl.value.trim(); // 读取并去除两端空格的昵称
const content = contentEl.value.trim(); // 读取并去除两端空格的评论内容
if (!nick) { alert('请填写昵称'); nickEl.focus(); return; } // 必填校验:昵称
if (!content) { alert('请填写评论内容'); contentEl.focus(); return; } // 必填校验:内容
if (content.length > 300) { alert('评论过长(最多 300 字)'); return; } // 简单长度限制
const data = loadComments(); // 读取现有评论数组
data.push({ // 追加一条新评论
nick, // 昵称
content, // 评论内容
time: formatTime() // 记录当前时间字符串
});
saveComments(data); // 保存到 localStorage
renderComments(); // 重新渲染列表
formEl.reset(); // 清空表单(让学生看到“已提交”的效果)
nickEl.focus(); // 体验优化:光标回到昵称
});
// ===== 5. 初始化渲染 =====
renderComments(); // 页面加载后立即渲染一次,显示已有评论
comments.html 即可。STORAGE_KEY 策略)。textContent,确保只输出纯文本,顺带给学生强化“输出编码/安全渲染”的意识。localStorage.removeItem(STORAGE_KEY) 并 renderComments()。