feat: Rate 支持清空评分(#1302)

 Closes: #1293
This commit is contained in:
不如摸鱼去 2025-09-18 15:50:07 +08:00
parent 41dd4177b6
commit 1333bdac4f
8 changed files with 273 additions and 12 deletions

View File

@ -67,6 +67,14 @@ function changeValue({ value }) {
<wd-rate v-model="value" allow-half />
```
## 允许清空评分
设置 `clearable` 属性,当 clearable 属性设置为 true再次点击相同的值时可以将值重置为 0。
```html
<wd-rate v-model="value" clearable />
```
## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
@ -83,6 +91,7 @@ function changeValue({ value }) {
| disabled | 是否禁用 | boolean | - | false | - |
| disabled-color | 禁用的图标颜色 | string | - | linear-gradient(315deg, rgba(177,177,177,1) 0%,rgba(199,199,199,1) 100%) | - |
| allow-half | 是否允许半选 | boolean | - | false | 1.7.0 |
| clearable | 是否允许再次点击后清除 | boolean | - | false | $LOWEST_VERSION$ |
## Events

View File

@ -67,6 +67,14 @@ Set the `allowHalf` property.
<wd-rate v-model="value" allow-half />
```
## Allow Clear Rating
Set the `clearable` property. When clearable is set to true, clicking the same value again can reset the value to 0.
```html
<wd-rate v-model="value" clearable />
```
## Attributes
| Parameter | Description | Type | Options | Default | Version |
@ -83,6 +91,7 @@ Set the `allowHalf` property.
| disabled | Whether it's disabled | boolean | - | false | - |
| disabled-color | Disabled icon color | string | - | linear-gradient(315deg, rgba(177,177,177,1) 0%,rgba(199,199,199,1) 100%) | - |
| allow-half | Whether to allow half selection | boolean | - | false | 1.7.0 |
| clearable | Whether to allow clear rating | boolean | - | false | $LOWEST_VERSION$ |
## Events

View File

@ -1451,6 +1451,7 @@
"yue-xiu-qu": "Yuexiu",
"yue-xuan-ze": "Monthly selection",
"yun-xu-ban-xuan": "Allow half selection",
"yun-xu-qing-kong-ping-fen": "Allow clear rating",
"yun-xu-kong-zhi-bing-she-zhi-placeholder": "Allow null values and set placeholder",
"yy-nian-mm-yue-dd-ri": "YY year MM month DD day",
"yy-nian-mm-yue-dd-ri-0": "YY year MM month DD day",

View File

@ -1451,6 +1451,7 @@
"yue-xiu-qu": "越秀区",
"yue-xuan-ze": "月选择",
"yun-xu-ban-xuan": "允许半选",
"yun-xu-qing-kong-ping-fen": "允许清空评分",
"yun-xu-kong-zhi-bing-she-zhi-placeholder": "允许空值,并设置 placeholder",
"yy-nian-mm-yue-dd-ri": "YY年MM月DD日",
"yy-nian-mm-yue-dd-ri-0": "YY年MM月DD日",

View File

@ -36,6 +36,15 @@
<demo-block :title="$t('yun-xu-ban-xuan')">
<wd-rate v-model="value8" allow-half />
</demo-block>
<demo-block :title="$t('yun-xu-qing-kong-ping-fen')">
<view style="margin-bottom: 24rpx">
<wd-rate v-model="value9" clearable />
</view>
<view>
<wd-rate v-model="value10" clearable allow-half />
</view>
</demo-block>
</page-wraper>
</template>
<script lang="ts" setup>
@ -49,6 +58,8 @@ const value5 = ref<number>(4)
const value6 = ref<number>(3)
const value7 = ref<number>(5)
const value8 = ref<number>(2.5)
const value9 = ref<number>(3)
const value10 = ref<number>(3.5)
function changeValue1({ value }: any) {
console.log(value)

View File

@ -92,5 +92,12 @@ export const rateProps = {
* 类型: boolean
* 默认值: false
*/
allowHalf: makeBooleanProp(false)
allowHalf: makeBooleanProp(false),
/**
* clearable true 0
* 类型: boolean
* 默认值: false
*/
clearable: makeBooleanProp(false)
}

View File

@ -12,9 +12,9 @@
:name="isActive(rate) ? activeIcon : icon"
:size="size"
:custom-style="rate === '100%' ? iconActiveStyle : iconStyle"
@click="changeRate(index, false)"
@click="handleClick(index, false)"
/>
<view v-if="props.allowHalf" class="wd-rate__item-half" @click.stop="changeRate(index, true)">
<view v-if="props.allowHalf" class="wd-rate__item-half" @click.stop="handleClick(index, true)">
<wd-icon
custom-class="wd-rate__item-star"
:name="isActive(rate) ? activeIcon : icon"
@ -125,12 +125,30 @@ function computeActiveValue() {
}
activeValue.value = tempActiveValue
}
/**
* @description 点击icon触发组件的change事件
* @description 处理点击事件
* @param index 点击的索引
* @param isHalf 是否为半星
*/
function changeRate(index: number, isHalf: boolean) {
if (props.readonly || props.disabled) return
const value = isHalf ? index + 0.5 : index + 1
function handleClick(index: number, isHalf: boolean) {
const { readonly, disabled, clearable, allowHalf, modelValue } = props
if (readonly || disabled) return
let value = isHalf ? index + 0.5 : index + 1
// modelValue
if (clearable) {
const minValue = allowHalf ? 0.5 : 1
if (value === modelValue && value === minValue) {
value = 0
}
}
updateValue(value)
}
/**
* @description 设置评分值并触发事件
*/
function updateValue(value: number) {
emit('update:modelValue', value)
emit('change', {
value
@ -149,7 +167,8 @@ async function onTouchMove(event: TouchEvent) {
const isHalf = props.allowHalf && clientX - target.left! < itemWidth / 2
const value = isHalf ? targetIndex + 0.5 : targetIndex + 1
if (value >= 0.5) {
changeRate(targetIndex, isHalf)
const value = isHalf ? targetIndex + 0.5 : targetIndex + 1
updateValue(value)
}
}
}

View File

@ -302,9 +302,9 @@ describe('评分组件', () => {
await nextTick()
// 直接调用组件的 changeRate 方法,模拟点击第一个星星
// 直接调用组件的 handleClick 方法,模拟点击第一个星星
// 这比模拟触摸事件更可靠,因为触摸事件依赖于 DOM 元素的位置
await (wrapper.vm as any).changeRate(0, false)
await (wrapper.vm as any).handleClick(0, false)
// 验证事件
const emitted = wrapper.emitted() as Record<string, any[]>
@ -326,9 +326,9 @@ describe('评分组件', () => {
await nextTick()
// 直接调用组件的 changeRate 方法,模拟点击第一个半星
// 直接调用组件的 handleClick 方法,模拟点击第一个半星
// 这比模拟触摸事件更可靠,因为触摸事件依赖于 DOM 元素的位置
await (wrapper.vm as any).changeRate(0, true)
await (wrapper.vm as any).handleClick(0, true)
// 验证事件
const emitted = wrapper.emitted() as Record<string, any[]>
@ -413,4 +413,208 @@ describe('评分组件', () => {
expect(wrapper.attributes('style')).toBe(customStyle)
})
// 测试清空功能
describe('clearable 功能测试', () => {
// 测试基本清空功能
test('clearable 为 true 时,点击相同的最小值可以清空', async () => {
const wrapper = mount(WdRate, {
props: {
modelValue: 1,
clearable: true
}
})
await nextTick()
// 调用 handleClick 方法模拟点击第一个星星当前值为1最小值为1
await (wrapper.vm as any).handleClick(0, false)
// 验证清空事件
const emitted = wrapper.emitted() as Record<string, any[]>
expect(emitted['update:modelValue']).toBeTruthy()
expect(emitted['update:modelValue'][0][0]).toBe(0)
expect(emitted['change']).toBeTruthy()
expect(emitted['change'][0][0]).toEqual({ value: 0 })
})
test('clearable 为 false 时,点击相同值不会清空', async () => {
const wrapper = mount(WdRate, {
props: {
modelValue: 1,
clearable: false
}
})
await nextTick()
// 调用 handleClick 方法,模拟点击第一个星星
await (wrapper.vm as any).handleClick(0, false)
// 验证不会清空而是设置为1
const emitted = wrapper.emitted() as Record<string, any[]>
expect(emitted['update:modelValue']).toBeTruthy()
expect(emitted['update:modelValue'][0][0]).toBe(1)
expect(emitted['change']).toBeTruthy()
expect(emitted['change'][0][0]).toEqual({ value: 1 })
})
// 测试半星模式下的清空
test('clearable + allowHalf 组合下,点击 0.5 可以清空', async () => {
const wrapper = mount(WdRate, {
props: {
modelValue: 0.5,
clearable: true,
allowHalf: true
}
})
await nextTick()
// 调用 handleClick 方法模拟点击第一个半星当前值为0.5最小值为0.5
await (wrapper.vm as any).handleClick(0, true)
// 验证清空事件
const emitted = wrapper.emitted() as Record<string, any[]>
expect(emitted['update:modelValue']).toBeTruthy()
expect(emitted['update:modelValue'][0][0]).toBe(0)
expect(emitted['change']).toBeTruthy()
expect(emitted['change'][0][0]).toEqual({ value: 0 })
})
test('非最小值时点击不会清空', async () => {
const wrapper = mount(WdRate, {
props: {
modelValue: 2,
clearable: true
}
})
await nextTick()
// 调用 handleClick 方法模拟点击第一个星星当前值为2点击值为1不等于当前值
await (wrapper.vm as any).handleClick(0, false)
// 验证不会清空而是设置为1
const emitted = wrapper.emitted() as Record<string, any[]>
expect(emitted['update:modelValue']).toBeTruthy()
expect(emitted['update:modelValue'][0][0]).toBe(1)
expect(emitted['change']).toBeTruthy()
expect(emitted['change'][0][0]).toEqual({ value: 1 })
})
// 测试边界情况
test('当前值为 0 时点击不会触发清空', async () => {
const wrapper = mount(WdRate, {
props: {
modelValue: 0,
clearable: true
}
})
await nextTick()
// 调用 handleClick 方法,模拟点击第一个星星
await (wrapper.vm as any).handleClick(0, false)
// 验证设置为1而不是保持0
const emitted = wrapper.emitted() as Record<string, any[]>
expect(emitted['update:modelValue']).toBeTruthy()
expect(emitted['update:modelValue'][0][0]).toBe(1)
expect(emitted['change']).toBeTruthy()
expect(emitted['change'][0][0]).toEqual({ value: 1 })
})
test('点击非最小值时不会清空', async () => {
const wrapper = mount(WdRate, {
props: {
modelValue: 3,
clearable: true
}
})
await nextTick()
// 调用 handleClick 方法模拟点击第三个星星当前值为3点击值为3但不是最小值
await (wrapper.vm as any).handleClick(2, false)
// 验证不会清空而是保持为3
const emitted = wrapper.emitted() as Record<string, any[]>
expect(emitted['update:modelValue']).toBeTruthy()
expect(emitted['update:modelValue'][0][0]).toBe(3)
expect(emitted['change']).toBeTruthy()
expect(emitted['change'][0][0]).toEqual({ value: 3 })
})
// 测试与其他状态的组合
test('readonly + clearable 时不会清空', async () => {
const wrapper = mount(WdRate, {
props: {
modelValue: 1,
clearable: true,
readonly: true
}
})
await nextTick()
// 调用 handleClick 方法,模拟点击第一个星星
await (wrapper.vm as any).handleClick(0, false)
// 验证在只读状态下不会触发任何事件
const emitted = wrapper.emitted() as Record<string, any[]>
expect(emitted['update:modelValue']).toBeFalsy()
expect(emitted['change']).toBeFalsy()
})
test('disabled + clearable 时不会清空', async () => {
const wrapper = mount(WdRate, {
props: {
modelValue: 1,
clearable: true,
disabled: true
}
})
await nextTick()
// 调用 handleClick 方法,模拟点击第一个星星
await (wrapper.vm as any).handleClick(0, false)
// 验证在禁用状态下不会触发任何事件
const emitted = wrapper.emitted() as Record<string, any[]>
expect(emitted['update:modelValue']).toBeFalsy()
expect(emitted['change']).toBeFalsy()
})
// 测试半星模式下非最小值不会清空
test('半星模式下点击非最小值不会清空', async () => {
const wrapper = mount(WdRate, {
props: {
modelValue: 1.5,
clearable: true,
allowHalf: true
}
})
await nextTick()
// 调用 handleClick 方法模拟点击第二个半星当前值为1.5点击值为1.5但不是最小值0.5
await (wrapper.vm as any).handleClick(1, true)
// 验证不会清空而是保持为1.5
const emitted = wrapper.emitted() as Record<string, any[]>
expect(emitted['update:modelValue']).toBeTruthy()
expect(emitted['update:modelValue'][0][0]).toBe(1.5)
expect(emitted['change']).toBeTruthy()
expect(emitted['change'][0][0]).toEqual({ value: 1.5 })
})
})
})