用Vue做一个可五星评分组件 - 两种思路

502 阅读4分钟

讨论两种思路

效果动图

颜色可调,可鼠标移动点击评分

2024-07-09 20-26-07.gif

图层定位

  1. 创建一个div标签装下☆☆☆☆☆作为底层图层
  2. 再创建一个div标签装下★★★★★作为覆盖图层
  3. 通过CSS的 position: absolute 将覆盖图层显示在底层图层上方,通过修改覆盖图层的宽度来显示评分多少

三元运算符

  1. 使用 v-for="i in 5" 创建 5 个span标签,分别装下一颗星
  2. 利用三元运算符控制 span 标签显示的星星为实心还是空心 i <= 当前停留星星索引 ? '★' : '☆'

共同思路

参数传递

  1. 使用 defineProps 接受一个默认评分值:score 和星星颜色: theme

defineProps 是 Vue 3 中 Composition API 提供的一个功能,用于在组合式函数组件(Composition API)中定义和接收父组件传递的 props。它与 Vue 2 中使用的 props 选项功能类似,但在语法和使用方式上有所不同,更加适合组合式 API 的风格。

defineProps 在组合式 API 函数组件中使用 props 更加简洁和直观。它不仅支持静态类型定义,还支持通过 TypeScript 或 JSDoc 注释进行类型检查。此外,结合 withDefaults 函数还可以为 props 提供默认值。掌握这些特性能够大大提高开发效率和代码的可维护性。

let props = defineProps({
  value: Number,
  theme: { type: String, default: 'orange' }
})
  1. 使用 defineEmits 接受一个修改评分的事件

defineEmits 是 Vue 3 Composition API 提供的一个函数,用于在组合式 API 中定义和触发自定义事件。它类似于 Vue 2 中的 $emit 方法,但在组合式 API 中使用更加直观和方便。

let emits = defineEmits([
  'update-rate'
])

自定义星星颜色

  • 这里星星用的是emoji表情来实现,实际上也是文本,因此只需要改变文本color属性即可。为了便于管理我们可以创建一个对象用键值对来管理星星颜色。
const themeObj = {
  orange: '#fa541c',
  blue: '#40a9ff',
  green: '#73d13d',
  black: '#00ff00',
  red: '#f5222d',
  yellow: 'yellow'
}
  • 定义一个变量,用于将接收到 theme 的值转换成CSS样式。
const fontStyle = computed(() => {
  return `color: ${themeObj[props.theme]};`
})
  • 将该样式绑定到父容器上,子元素可用继承父元素的文本颜色属性。
<div :style="fontStyle">
    <!-- 两种思路实现 -->
</div>

鼠标移动评分

首先要实现这个功能,需要用到鼠标的三个触发事件: 移入 移除 点击.
以及三个对应参数: 传入的 value,用于记录当前鼠标停留在第几颗星星的索引 Index,v-for="(i in 5)" 的 i

1. 移入(mouseover): 将该事件添加到子元素上,当鼠标移动到 '空心星星' 上时我们需要将 '空心星星' 显示成 '实心星星' ,触发事件调用方法实时修改 Index 的值:

function mouseOver(i) {
  Index = i
  console.log("移动事件( i , index ): ", i, Index)
}

2. 移出(mouseout): 将该事件添加到父元素上,当鼠标离开了父元素的块级,将 Index 重置为 value的值,以实现取消评分的功能:

function mouseOut() {
  Index = value
  console.log("移出事件( index , value ): ", Index, value)
}

3. 点击(click): 将该事件添加到子元素上,当子元素被点击时触发 修改评分事件 ,Vue中组件通信是异步事件,我们需要等待修改value成功后再进行下一步。在父组件中的修改value的方法应该对接收到的value值做判断,这里不属于我们讨论的范畴就深入讨论:

const onRate = async () => {
  await emits('update-rate', state.scoreIndex)
  console.log("修改事件( index , value ): ", state.scoreIndex, props.value)
}

实现评分组件

用图层定位来实现

用v-for 生成一组空星和一组实星

<div :style="fontStyle">
      <div class="rate" @mouseout="mouseOut">
          // 空心底层图层  
          <span @mouseover="mouseOver(i)" v-for="i in 5" :key="i"></span>
          
          // 实心覆盖图层
          <span class="hollow" :style="fontWidth">
              <span @click="onRate(i)" @mouseover="mouseOver(i)" v-for="i in 5" :key="i"></span>
          </span>
          
      </div>
  </div>

利用CCS样式将覆盖图层显示到底层之上

  .rate {
      position: relative;
      display: block;
  }
  .rate span{
      display: inline-block;
      width: 1rem;
      height: 22px;
      cursor: pointer;
      overflow: hidden;
  }
  .rate > span.hollow {
      position: absolute;
      display:block;
      top:0;
      left:0;
      width:0;
      overflow:hidden;
  }

联合 js 控制覆盖图层的宽度

  • 因为这里星星是 emoji表情 实际上来说就是文本,也就是 1em 大小,所以我们可以用 Indexem拼接组成CSS样式,并绑定给覆盖图层。
<span class="hollow" :style="fontWidth">
    <span @click="onRate(i)" @mouseover="mouseOver(i)" v-for="i in 5" :key="i"></span>
</span>
let fontWidth = computed(() => `width: ${Index}em;`)

完整代码

父组件调用示例请滑至最底部

用三元运算符来实现

只生成一组星星标签

我们用三元运算符判断当前标签的 i 是否满足 i <= Index,满足的显示实星,不满足显示空心。Index 需要给定一个初始值为传入的 value 的值,我们利用 onMounted() 来实现在组件完成初始渲染并创建 DOM 节点后运行赋值代码

onMounted(() => {
  Index = props.value
})

Html部分代码

  <h1 :style="fontStyle">
  
    <div class="show" @mouseout="mouseOut">
      <span v-for="i in 5" :key="i" @mouseover="mouseOver(i)" @click="onRate(i)">{{ i <= state.scoreIndex ? '★' : '☆'}}</span>
    </div>

  </h1>

CSS样式部分

将鼠标样式改为可点击样式

.show {
  display: flex;
  padding: 10px 20px;
  background-color: #f5f5f5;
  border-radius: 5px;
  box-shadow: 0 0 5px #ccc;
}

.show>span {
  cursor: pointer;
}

完整代码

父组件调用示例请滑至最底部

父组件调用示例

<template>
  <div>
    <Rate :value="score" theme="orange" @update-rate="update" />
  </div>
</template>
import Rate from './components/Rate.vue'
import { ref } from 'vue'

let score = ref(3)

function update(num) {
  if (num > 5 || num < 1) {
    return
  }
  score.value = num
}

留个赞ba