yunshangxie/tuniao-ui/components/tn-rate/tn-rate.vue

326 lines
8.4 KiB
Vue
Raw Permalink Normal View History

2023-12-25 17:56:30 +08:00
<template>
<view :id="elId" class="tn-rate-class tn-rate" @touchmove.stop.prevent="touchMove">
<view class="tn-rate__wrap" :class="[elClass]" v-for="(item, index) in count" :key="index">
<view class="tn-rate__wrap__icon"
:class="[`tn-icon-${(allowHalf && halfIcon ? activeIndex > index + 1 : activeIndex > index) ? elActionIcon : elInactionIcon}`]"
:style="[iconStyle(index)]" @tap="click(index + 1, $event)">
<!-- 半图标 -->
<view v-if="showHalfIcon(index)" class="tn-rate__wrap__icon--half" :class="[`tn-icon-${elActionIcon}`]"
:style="[halfIconStyle]"></view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-rate',
props: {
// 选中星星的数量
value: {
type: Number,
default: 0
},
// 显示的星星数
count: {
type: Number,
default: 5
},
// 最小能选择的星星数
minCount: {
type: Number,
default: 0
},
// 禁用状态
disabled: {
type: Boolean,
default: false
},
// 是否可以选择半星
allowHalf: {
type: Boolean,
default: false
},
// 星星大小
size: {
type: Number,
default: 32
},
// 被选中的图标
activeIcon: {
type: String,
default: 'star-fill'
},
// 未被选中的图标
inactiveIcon: {
type: String,
default: 'star'
},
// 被选中的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 默认颜色
inactiveColor: {
type: String,
default: '#AAAAAA'
},
// 星星之间的距离
gutter: {
type: Number,
default: 10
},
// 自定义颜色
colors: {
type: Array,
default () {
return []
}
},
// 自定义图标
icons: {
type: Array,
default () {
return []
}
}
},
computed: {
// 图标显示的比例
showHalfIcon(index) {
return index => {
return this.allowHalf && Math.ceil(this.activeIndex) === index + 1 && this.halfIcon
}
},
// 被激活的图标
elActionIcon() {
const len = this.icons.length
// icons参数传递了3个图标当选中2个时用第一个图标4个时用第二个图标
// 5个时用第三个图标作为激活的图标
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.icons[0]
if (step > len) return this.icons[len - 1]
return this.icons[step - 1]
}
return this.activeIcon
},
// 未被激活的图标
elInactionIcon() {
const len = this.icons.length
// icons参数传递了3个图标当选中2个时用第一个图标4个时用第二个图标
// 5个时用第三个图标作为激活的图标
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.icons[0]
if (step > len) return this.icons[len - 1]
return this.icons[step - 1]
}
return this.inactiveIcon
},
// 被激活的颜色
elActionColor() {
const len = this.colors.length
// 如果有设置colors参数(此参数用于将图标分段比如一共5颗星colors传3个颜色值那么根据一定的规则2颗星可能为第一个颜色
// 4颗星为第二个颜色值5颗星为第三个颜色值)
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.colors[0]
if (step > len) return this.colors[len - 1]
return this.colors[step - 1]
}
return this.activeColor
},
// 图标的样式
iconStyle() {
return index => {
let style = {}
style.fontSize = this.$tn.string.getLengthUnitValue(this.size)
style.padding = `0 ${this.$tn.string.getLengthUnitValue(this.gutter / 2)}`
// 当前图标的颜色
if (this.allowHalf && this.halfIcon) {
style.color = this.activeIndex > index + 1 ? this.elActionColor : this.inactiveColor
} else {
style.color = this.activeIndex > index ? this.elActionColor : this.inactiveColor
}
return style
}
},
// 半图标样式
halfIconStyle() {
let style = {}
style.fontSize = this.$tn.string.getLengthUnitValue(this.size)
style.padding = `0 ${this.$tn.string.getLengthUnitValue(this.gutter / 2)}`
style.color = this.elActionColor
return style
}
},
data() {
return {
// 保证控件的唯一性
elId: this.$tn.uuid(),
elClass: this.$tn.uuid(),
// 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
starBoxLeft: 0,
// 当前激活的星星的序号
activeIndex: this.value,
// 每个星星的宽度
starWidth: 0,
// 每个星星最右边到盒子组件最左边的距离
starWidthArr: [],
// 标记是否为半图标
halfIcon: false,
}
},
watch: {
value: {
handler(newName, oldName) {
this.activeIndex = newName
if (this.allowHalf && (newName % 1 === 0.5)) {
this.halfIcon = true
} else {
this.halfIcon = false
}
},
immediate: true
},
size() {
// 当尺寸修改的时候重新获取布局尺寸信息
this.$nextTick(() => {
this.getElRectById()
this.getElRectByClass()
})
}
},
mounted() {
this.getElRectById()
this.getElRectByClass()
},
methods: {
// 获取评分组件盒子的布局信息
getElRectById() {
this._tGetRect('#' + this.elId).then(res => {
this.starBoxLeft = res.left
})
},
// 获取单个星星的尺寸
getElRectByClass() {
this._tGetRect('.' + this.elClass).then(res => {
this.starWidth = res.width
// 把每个星星最右边到盒子最左边的距离
for (let i = 0; i < this.count; i++) {
this.starWidthArr[i] = (i + 1) * this.starWidth
}
})
},
// 手指滑动
touchMove(e) {
if (this.disabled) return
if (!e.changedTouches[0]) return
const movePageX = e.changedTouches[0].pageX
// 滑动点相对于评分盒子左边的距离
const distance = movePageX - this.starBoxLeft
// 如果滑动到了评分盒子的左边界设置为0星
if (distance <= 0) {
this.activeIndex = 0
}
// 计算滑动的距离相当于点击多少颗星星
let index = Math.ceil(distance / this.starWidth)
if (this.allowHalf) {
const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
if (distance < iconHalfWidth) {
this.halfIcon = true
index -= 0.5
} else {
this.halfIcon = false
}
}
this.activeIndex = index > this.count ? this.count : index
if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
this.emitEvent()
},
// 通过点击直接选中
click(index, e) {
if (this.disabled) return
// 半星选择
if (this.allowHalf) {
if (!e.changedTouches[0]) return
const movePageX = e.changedTouches[0].pageX
// 点击点相对于当前图标左边的距离
const distance = movePageX - this.starBoxLeft
const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
if (distance < iconHalfWidth) {
this.halfIcon = true
} else {
this.halfIcon = false
}
}
// 对第一个星星特殊处理只有一个的时候点击可以取消否则无法作0星评价
if (index == 1) {
if (this.allowHalf && this.allowHalf) {
if ((this.activeIndex === 0.5 && this.halfIcon) ||
(this.activeIndex === 1 && !this.halfIcon)) {
this.activeIndex = 0
} else {
this.activeIndex = this.halfIcon ? 0.5 : 1
}
} else {
if (this.activeIndex == 1) {
this.activeIndex = 0
} else {
this.activeIndex = 1
}
}
} else {
this.activeIndex = (this.allowHalf && this.halfIcon) ? index - 0.5 : index
}
if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
this.emitEvent()
},
// 发送事件
emitEvent() {
this.$emit('change', this.activeIndex)
// 修改v-model的值
this.$emit('input', this.activeIndex)
}
}
}
</script>
<style lang="scss" scoped>
.tn-rate {
display: inline-flex;
align-items: center;
margin: 0;
padding: 0;
&__wrap {
&__icon {
position: relative;
box-sizing: border-box;
&--half {
position: absolute;
top: 0;
left: 0;
display: inline-block;
overflow: hidden;
width: 50%;
}
}
}
}
</style>