625 lines
15 KiB
Plaintext
625 lines
15 KiB
Plaintext
<template>
|
|
<!-- TODO Android/iOS平台不支持裁剪显示半个文字,高性能场景包裹一层view不合适,需要等App平台解决-->
|
|
<text v-if="props.full!=true" class="active-color" :class="props.starActiveClass ?? ''" :style="props.starActiveStyle ?? ''">{{ stars }}</text> <!-- 高性能,单层,只读,不显示灰星,不支持小数。必须给value-->
|
|
<view v-else
|
|
<!-- #ifdef MP -->
|
|
:id="containerId"
|
|
<!-- #endif -->
|
|
style="position: relative;align-self: flex-start;" @click="onTap">
|
|
<text class="stars-pos gray-stars" style="white-space: nowrap;" :class="props.starClass ?? ''" :style="props.starStyle ?? ''">{{ stars }}</text> <!-- 背景灰星-->
|
|
<!-- Android/iOS平台不支持裁剪显示半个文字,需通过包裹一层view裁剪实现 -->
|
|
<view class="stars-pos active-init-width" :style="'width:'+calculatedWidth">
|
|
<text v-if="props.full" class="active-color" :class="props.starActiveClass ?? ''"
|
|
:style="'white-space: nowrap; align-self: flex-start;' + props.starStyle + ';' + props.starActiveStyle">★★★★★</text>
|
|
</view>
|
|
<!-- 前景亮星,宽度根据value变化-->
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
/**
|
|
* Rate 评分
|
|
* @description 评分组件
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
defineOptions({
|
|
externalClasses: ['star-class', 'star-active-class']
|
|
})
|
|
|
|
const props = defineProps<{
|
|
/**
|
|
* @description 当前评分,取值范围 0~5 的数字
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
value?: number,
|
|
/**
|
|
* @description v-model 绑定值,支持双向绑定,优先级高于 value
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
modelValue?: number,
|
|
/**
|
|
* @description 是否显示完整 5 颗星。为 true 时用灰星填充未选中位置;为 false 时仅显示亮星,性能更高但不支持点击交互
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
full?: boolean,
|
|
/**
|
|
* @description 是否只读,为 true 时不响应点击评分(full 为 false 时始终只读)
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
readonly?: boolean,
|
|
/**
|
|
* @description 灰星是否使用镂空样式(☆),为 false 时使用实心灰星(★)
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
plain?: boolean,
|
|
/**
|
|
* @description 灰星(未选中)的自定义样式类
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
starClass?: string.ClassString,
|
|
/**
|
|
* @description 灰星(未选中)的内联样式
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
starStyle?: string
|
|
/**
|
|
* @description 亮星(选中)的自定义样式类
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
starActiveClass?: string.ClassString,
|
|
/**
|
|
* @description 亮星(选中)的内联样式
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
starActiveStyle?: string
|
|
}>()
|
|
|
|
// 计算星星显示
|
|
const stars = computed(() => {
|
|
if (props.full!=true) {
|
|
const value = Math.max(0, Math.min(5, props.value ?? 0))
|
|
return '★'.repeat(Math.round(value)) //返回亮星数量
|
|
} else {
|
|
return props.plain == true ? '☆'.repeat(5) : '★'.repeat(5) //返回灰星数量
|
|
}
|
|
})
|
|
|
|
// 问题:props.full != true 时,下面的Script其实都没有意义,但却需要执行。但vue里又无法用if包裹下面的代码,不灵活
|
|
|
|
const emit = defineEmits<{
|
|
/**
|
|
* @description 评分变化事件,参数为评分值,类型为 number
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
change: [value: number]
|
|
/**
|
|
* @description v-model 绑定值变化事件,参数为评分值,类型为 number
|
|
* @uniPlatform {
|
|
* "app": {
|
|
* "android": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.21"
|
|
* },
|
|
* "ios": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.11"
|
|
* },
|
|
* "harmony": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07",
|
|
* "unixVaporVer": "5.07"
|
|
* }
|
|
* },
|
|
* "mp": {
|
|
* "weixin": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* },
|
|
* "alipay": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.31"
|
|
* }
|
|
* },
|
|
* "web": {
|
|
* "uniVer": "x",
|
|
* "unixVer": "5.07"
|
|
* }
|
|
* }
|
|
*/
|
|
'update:modelValue': [value: number]
|
|
}>()
|
|
|
|
// #ifdef MP
|
|
const instance = getCurrentInstance()
|
|
const containerId = `uni_rate_${instance?.uid || 'default'}`
|
|
// #endif
|
|
|
|
// 响应式数据 - 优先使用modelValue,其次使用value
|
|
const currentValue = ref<number>(0)
|
|
currentValue.value = props.modelValue ?? props.value ?? 0
|
|
// 安全初始化函数
|
|
/* function safeInitValue() {
|
|
const initialValue = props.modelValue ?? props.value ?? 0
|
|
if (typeof initialValue === 'number' && !isNaN(initialValue)) {
|
|
currentValue.value = initialValue
|
|
} else {
|
|
currentValue.value = 0
|
|
}
|
|
}
|
|
// 初始化
|
|
safeInitValue() */
|
|
// 监听props变化
|
|
watch(() : number | null => {
|
|
return props.modelValue ?? props.value ?? null
|
|
}, (newValue : number | null) => {
|
|
if (newValue != null && typeof newValue === 'number' && !isNaN(newValue as number)) {
|
|
currentValue.value = newValue as number
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// 计算点亮层的宽度
|
|
const calculatedWidth = computed(() => {
|
|
// 确保在 0 到 5 之间
|
|
const value = currentValue.value
|
|
const rate = Math.max(0, Math.min(5, value))
|
|
// console.log('rate: ',rate);
|
|
// TODO Android端小数无法正常渲染,不是宽度计算的问题
|
|
return `${(rate / 5) * 100}%`
|
|
})
|
|
|
|
// #ifndef MP
|
|
// 处理点击事件
|
|
function onTap(event: UniPointerEvent) : void {
|
|
// 如果是禁用模式,不处理点击
|
|
if (props.readonly == true) {
|
|
return
|
|
}
|
|
|
|
// 获取点击位置 - 优化的事件处理
|
|
let clickX = event.x
|
|
|
|
// 获取容器宽度 - 使用 currentTarget 获取绑定事件的 view 容器,而不是被点击的 text 元素
|
|
let containerWidth = 0
|
|
try {
|
|
if (event.currentTarget?.offsetWidth != null && (event.currentTarget as UniElement).offsetWidth > 0) {
|
|
containerWidth = (event.currentTarget as UniElement).offsetWidth
|
|
}
|
|
} catch (e) {
|
|
// 忽略错误,使用默认值
|
|
console.log('e: ',e);
|
|
}
|
|
// console.log("containerWidth:" + containerWidth);
|
|
|
|
// 计算点击的是第几颗星(从0开始)
|
|
const finalValue = Math.round(clickX / containerWidth * 5)
|
|
// console.log('finalValue: ',finalValue);
|
|
|
|
// 计算每颗星的宽度
|
|
// const starWidth = containerWidth / 5
|
|
// 计算点击位置在星内的偏移量
|
|
// const offsetInStar = (clickX % starWidth) / starWidth
|
|
/* // 根据偏移量决定是整星还是半星
|
|
let newValue: number
|
|
if (offsetInStar <= 0.5) {
|
|
newValue = starIndex + 0.5 // 半星
|
|
} else {
|
|
newValue = starIndex + 1 // 整星
|
|
}
|
|
// 确保值在有效范围内 */
|
|
// const finalValue = Math.max(0.5, Math.min(5, starIndex + 1))
|
|
|
|
// 更新值
|
|
currentValue.value = finalValue
|
|
|
|
// 触发change事件
|
|
emit('change', finalValue)
|
|
|
|
// 触发v-model更新
|
|
emit('update:modelValue', finalValue)
|
|
}
|
|
// #endif
|
|
|
|
// #ifdef MP
|
|
async function onTap(event: UniPointerEvent) : Promise<void> {
|
|
if (props.readonly == true) {
|
|
return
|
|
}
|
|
|
|
const rect = await uni.getElementById(containerId)?.getBoundingClientRectAsync()
|
|
if (rect == null || rect.width <= 0) {
|
|
return
|
|
}
|
|
|
|
const localX = event.clientX - rect.left
|
|
if (!isFinite(localX)) {
|
|
return
|
|
}
|
|
|
|
const clampedX = Math.max(0, Math.min(rect.width, localX))
|
|
const finalValue = Math.max(0, Math.min(5, Math.round(clampedX / rect.width * 5)))
|
|
|
|
currentValue.value = finalValue
|
|
emit('change', finalValue)
|
|
emit('update:modelValue', finalValue)
|
|
}
|
|
// #endif
|
|
|
|
// TODO 接入form表单规范,可以submit
|
|
</script>
|
|
|
|
<style>
|
|
|
|
.stars-pos {
|
|
/* 确保两个 star 的位置重叠 */
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
overflow: hidden; /* 关键:裁剪点亮层 */
|
|
}
|
|
.gray-stars {
|
|
color: #ccc; /* 未点亮颜色 */
|
|
position: relative; /* 确保底层可见 */
|
|
}
|
|
.active-init-width {
|
|
width: 0%; /* full时高亮星的初始宽度 */
|
|
}
|
|
.active-color{
|
|
color: #ffca3e; /* 点亮颜色 */
|
|
}
|
|
</style>
|