最稳妥结构是用语义化<section>包裹商品列表,每个商品用<article>,内含<h3>、<span>积分值和<button>兑换按钮;点击瞬间禁用按钮并更新文案,用AbortController防重复提交;积分数字用<span class="points">配合font-variant-numeric保持等宽对齐。
直接用语义化 <section> 包裹商品列表,别套多层 <div>。积分商品本质是「可筛选、可排序、带状态(兑完/限购)的列表」,不是杂志排版——过度 flex 嵌套或 grid 复杂行列反而让 aria-live 读屏、键盘导航和后续 JS 状态管理变麻烦。
推荐结构:<main> 下放一个 <section aria-label="积分商品列表">,每个商品用 <article>,内含 <h3>(商品名)、<span>(积分值)、<button>(兑换按钮)。这样既满足 SEO 基础,又方便用 document.querySelectorAll('article') 批量操作。
用户点击兑换后,按钮要立刻变灰禁用、文字变「处理中」,但后端接口可能延迟。别等 fetch() 返回再改 UI——那会让人误点两次。
button.disabled = true、button.textContent = '处理中...'
AbortController 防重复提交,避免用户狂点触发多次请求button.disabled = false、button.textContent = '兑换',并用 alert() 或 toast 提示(别只靠 console)别用 position: absolute 把积分值盖在图片右上角——它会脱离文档流,导致屏幕阅读器跳过,也影响响应式断点下的换行对齐。
立即学习“前端免费学习笔记(深入)”;
更稳的做法:
<span class="points">2800 <small>积分</small></span>
font-variant-numeric: tabular-nums 让数字等宽,避免「1000」和「999」宽度不一致导致列表错位--points-color: #e63946,方便运营临时换主题色.points { color: var(--points-color); font-variant-numeric: tabular-nums; font-weight: bold;}
前端校验只是体验优化,后端必须二次验证。但前端拦截能省掉一次无意义请求,也避免按钮点了没反应引发焦虑。
检查时机有两个关键点:
localStorage.getItem('userPoints') 和 localStorage.getItem('isLoggedIn') 初始化按钮状态(比如积分 disabled 和 title="积分不足")CustomEvent 触发重算:document.dispatchEvent(new CustomEvent('points-updated'))
aria-hidden="true" + tabindex="-1" 真正移出焦点流/api/rewards/status?id=123,或者让后端在兑换响应里附带最新 stock 和 limit_reached 字段。没这步,用户刷着页面就看到「已兑完」又变回「立即兑换」,信任感直接掉一半。