星级评分系统是前端界面中常见的交互组件,它的核心并不只是展示五颗星,而是通过一组明确的状态规则响应用户操作。用户将鼠标移动到某个评分项上时,界面需要预先展示该分值对应的效果;鼠标离开后,界面要回到已经确认的评分状态;当用户点击某个评分项时,当前分值被固定下来,并同步到提示文本中。只要把结构、样式和事件三个层面拆解清楚,就可以用原生JavaScript完成一个稳定、易维护的评分组件。

一、页面结构:让每个评分项都携带明确分值
在搭建结构时,需要先确定组件由哪几部分组成。一个评分容器负责包裹所有评分项,一个提示区域负责显示当前评分,若干个评分项则分别代表一分到五分。为了让脚本能够准确识别每个评分项对应的分值,可以在每个评分项上添加自定义属性,例如 data-value。相比把分值写在文本内容里,再通过字符串解析,自定义属性更清晰,也更利于后续维护。
为了使结构保持轻量,可以使用 <div> 作为容器,使用 <span> 作为评分项,并使用 <p> 显示评分文本。这里的 <span> 是普通HTML元素,不是函数调用。每个评分项默认显示中文“星”,并通过类名区分普通状态和高亮状态。这样即使不使用图片字体,也可以表达评分含义。
<div class="star-rating"> <span class="star" data-value="1">星</span> <span class="star" data-value="2">星</span> <span class="star" data-value="3">星</span> <span class="star" data-value="4">星</span> <span class="star" data-value="5">星</span> <p class="rating-text">当前评分:0</p> </div>
这段结构的关键在于所有评分项都拥有相同的类名和不同的 data-value。脚本后续可以通过类名批量获取节点,再通过自定义属性读取分值。提示文本初始显示零分,代表用户尚未提交评分。这样的结构既简单,又能清楚表达评分组件的基本语义。
二、样式设计:用类名表达默认、预览与确认状态
样式层面要解决的是视觉反馈问题。默认状态下,评分项使用较浅的颜色,表示尚未激活;当某个评分项进入高亮状态时,颜色变为更醒目的暖色,表示当前预览或已确认的分值。为了让状态切换更自然,可以给颜色变化加入短暂的过渡效果。同时,将鼠标指针设置为手型,可以提示用户该区域可点击。
在具体实现中,推荐通过一个状态类名来控制高亮,例如 active。JavaScript只需要增加或移除这个类名,而不需要直接修改每个节点的内联样式。这样可以让样式规则集中在CSS中,脚本只负责状态切换,代码职责更清晰,也方便后期统一调整视觉风格。
.star-rating {
font-size: 24px;
color: #ccc;
cursor: pointer;
}
.star {
margin-right: 5px;
transition: color 0.2s;
}
.star.active {
color: #ffc107;
}
.rating-text {
margin-top: 10px;
font-size: 16px;
}
以上样式中,容器设置了统一字号和默认颜色,评分项之间保留少量间距,提示文本与评分项之间也保留了适当距离。由于高亮状态完全依赖 active 类名,后续无论悬停预览还是点击确认,都只需要操作同一个类名即可。这种写法减少了重复样式判断,也让组件状态更容易理解。
三、脚本逻辑:用事件驱动评分状态
JavaScript部分的核心是一个状态变量和三类事件。状态变量 currentRating 保存用户已经确认的评分,初始值为零。悬停事件只负责临时预览,不应该修改这个变量;点击事件才负责更新变量。这样可以避免预览状态和确认状态互相干扰,也能让组件行为更加稳定。
在获取节点时,可以使用 document.querySelectorAll 获取所有评分项,使用 document.querySelector 获取提示文本。为了避免重复编写高亮逻辑,可以抽取一个渲染函数,根据传入的分值遍历所有评分项。当评分项的分值小于或等于目标分值时,添加高亮类名;否则移除高亮类名。这样无论是悬停、离开还是点击,都可以复用同一套渲染逻辑。
// 获取所有评分项与提示文本
const stars = document.querySelectorAll('.star');
const ratingText = document.querySelector('.rating-text');
// 当前已确认的评分,0表示尚未评分
let currentRating = 0;
// 根据传入的分值更新评分项的高亮状态
function renderStars(value) {
stars.forEach(item => {
const itemValue = parseInt(item.dataset.value, 10);
if (itemValue <= value) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
// 更新评分提示文本
function renderText(value) {
ratingText.textContent = '当前评分:' + value;
}
// 为每个评分项绑定交互事件
stars.forEach(star => {
// 鼠标悬停时预览当前分值效果
star.addEventListener('mouseover', function() {
const hoverValue = parseInt(this.dataset.value, 10);
renderStars(hoverValue);
});
// 鼠标移出时恢复已确认的评分状态
star.addEventListener('mouseout', function() {
renderStars(currentRating);
});
// 点击时确认评分,再次点击同一分值可取消
star.addEventListener('click', function() {
const clickValue = parseInt(this.dataset.value, 10);
if (clickValue === currentRating) {
currentRating = 0;
} else {
currentRating = clickValue;
}
renderStars(currentRating);
renderText(currentRating);
});
});
在这段脚本中,mouseover 事件根据当前悬停的评分项计算预览分值,并调用渲染函数展示效果。mouseout 事件并不关心鼠标移到了哪里,而是统一恢复到已确认的评分状态。这样做可以让界面始终围绕唯一的状态源进行更新,避免因为鼠标移动路径不同而产生不一致的视觉结果。
click 事件先判断当前点击的分值是否等于已确认分值,如果相等则取消评分,如果不相等则确认新的评分。这个判断实现了再次点击取消评分的能力,也让组件更接近真实表单场景中的操作习惯。提示文本会随着状态变化同步更新,使用户能够明确知道当前选择的分值。
四、整合思路与实现要点
将结构、样式和脚本组合后,整个组件的运行流程就非常清晰。页面加载后,脚本首先获取评分项和提示文本;当鼠标悬停时,界面展示临时预览;当鼠标移出时,界面恢复确认状态;当点击发生时,状态变量被更新,提示文本和评分项高亮同步变化。这种通过状态驱动视图的方式,使组件行为可预测,也方便后续调整。
在维护层面,有几个要点值得注意。首先,评分项数量变化时,只需要修改结构中的节点和对应的 data-value,脚本依然可以自动获取全部节点。其次,渲染函数集中处理高亮逻辑,避免了多个事件中重复编写相似代码。再次,提示文本通过 textContent 更新,能够保持内容简洁,也避免不必要的HTML解析。
在实际使用时,还可以根据页面风格调整颜色、字号和间距,也可以把中文“星”替换成其他视觉元素。不过,无论视觉表现如何变化,核心思路都保持一致:结构承载分值,样式表达状态,脚本监听事件并维护唯一评分状态。掌握这一套方法后,面对类似的悬停预览与点击确认组件,也可以快速拆解并完成实现。