2025/2/8第一次更新

This commit is contained in:
爱吃咸鱼小猫咪
2025-02-08 18:50:38 +08:00
commit d7af560866
26519 changed files with 5046029 additions and 0 deletions
@@ -0,0 +1,2 @@
export * from './rate-custom'
export * from './use-rate'
@@ -0,0 +1,69 @@
import { computed } from 'vue'
import { useComponentSize, useNamespace } from '../../../../hooks'
import { formatDomSizeValue } from '../../../../utils'
import type { CSSProperties } from 'vue'
import type { RateProps } from '../rate'
import type { RateItem } from '../types'
type ItemType = 'active' | 'inactive'
type ItemClassType = (type: ItemType, item: RateItem) => string
type ItemStyleType = (type: ItemType, item: RateItem) => CSSProperties
export const useRateCustomStyle = (props: RateProps) => {
const ns = useNamespace('rate')
const { sizeType } = useComponentSize(props.size)
// rate对应的类
const rateClass = computed<string>(() => {
const cls: string[] = [ns.b()]
if (props.size && sizeType.value === 'inner') cls.push(ns.m(props.size))
if (props.readonly) cls.push(ns.is('readonly'))
return cls.join(' ')
})
// item对应的类
const itemClass = computed<ItemClassType>(() => {
return (type, item) => {
const cls: string[] = [ns.e('item'), ns.is(type)]
if (item.color.class) cls.push(item.color.class)
return cls.join(' ')
}
})
// item对应的样式
const itemStyle = computed<ItemStyleType>(() => {
return (type, item) => {
const style: CSSProperties = {}
if (props.size && sizeType.value === 'custom')
style.fontSize = formatDomSizeValue(props.size)
if (props.gutter)
style.padding = `0rpx calc(${formatDomSizeValue(props.gutter)} / 2)`
if (type === 'active') {
if (!item.color.class)
style.color = item.color.style || 'var(--tn-color-primary)'
}
if (type === 'inactive') {
if (!item.color.class)
style.color = item.color.style || 'var(--tn-color-gray)'
}
return style
}
})
return {
ns,
rateClass,
itemStyle,
itemClass,
}
}
@@ -0,0 +1,70 @@
import { ref, toRef } from 'vue'
import { useComponentColor } from '../../../../hooks'
import { isEmptyVariableInDefault } from '../../../../utils'
import type { RateProps } from '../rate'
import type { RateItemData } from '../types'
const useRateItemData = (props: RateProps) => {
const rateItemData = ref<RateItemData[]>([])
// 生成RateItemData数据
const generateRateItemData = () => {
const {
max,
inactiveIcon,
activeIcon,
inactiveColor,
activeColor,
customData,
} = props
const defaultConfig = {
inactiveIcon,
activeIcon,
inactiveColor,
activeColor,
}
const data: RateItemData[] = []
for (let i = 0; i < max; i++) {
const configData = {
...defaultConfig,
...isEmptyVariableInDefault(customData?.[i], {}),
}
// 解析颜色值
const [inactiveColorClass, inactiveColorStyle] = useComponentColor(
toRef(configData, 'inactiveColor'),
'text'
)
const [activeColorClass, activeColorStyle] = useComponentColor(
toRef(configData, 'activeColor'),
'text'
)
data.push({
active: {
icon: configData.activeIcon,
color: {
class: activeColorClass.value,
style: activeColorStyle.value,
},
},
inactive: {
icon: configData.inactiveIcon,
color: {
class: inactiveColorClass.value,
style: inactiveColorStyle.value,
},
},
})
}
rateItemData.value = data
}
generateRateItemData()
return {
rateItemData,
}
}
export default useRateItemData
@@ -0,0 +1,193 @@
import { getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../../constants'
import { useSelectorQuery, useTouch } from '../../../../hooks'
import { debugWarn, generateId } from '../../../../utils'
import { useFormItem } from '../../../form'
import useRateItemData from './use-rate-item-data'
import type { SetupContext } from 'vue'
import type { RateEmits, RateProps } from '../rate'
export const useRate = (
props: RateProps,
emits: SetupContext<RateEmits>['emit']
) => {
const instance = getCurrentInstance()
if (!instance) {
debugWarn('TnRate', '请在setup函数中使用useRate')
}
// 生成唯一id
const componentId = `tr-${generateId()}`
const { getSelectorNodeInfo } = useSelectorQuery(instance)
const { formItem } = useFormItem()
const { rateItemData } = useRateItemData(props)
const {
currentX: rateCurrentX,
updateOptions: updateRateTouchOptions,
onTouchStart: rateTouchStartHandler,
onTouchMove: rateTouchMoveHandler,
onTouchEnd: rateTouchEndHandler,
} = useTouch()
// 更新选中的值
const updateValue = (value: number) => {
if (Number.isNaN(value)) {
debugWarn('TnRate', 'Rate回填数据发生错误')
value = 0
}
emits(UPDATE_MODEL_EVENT, value)
nextTick(() => {
emits(CHANGE_EVENT, value)
if (props.validateEvent) {
formItem?.validate('change').catch((err) => {
debugWarn('TnRate', `Rate验证发生错误: ${err}`)
})
}
})
}
// 容器的宽度
let componentItemWidth = 0
const activeItemWidth = ref<number>(0)
watch(
() => props.modelValue,
(val) => {
if (!props.allowHalf) {
val = Math.ceil(val)
}
activeItemWidth.value = val * componentItemWidth
}
)
let initCount = 0
// 获取组件和item的宽度信息
const getComponentRectInfo = async () => {
try {
const itemRectInfo = await getSelectorNodeInfo(
`#${componentId} .tn-rate__item`
)
if (!itemRectInfo?.width) {
throw new Error('获取组件容器宽度失败')
}
componentItemWidth = itemRectInfo.width || 0
const left = itemRectInfo.left || 0
updateRateTouchOptions({
left,
right: componentItemWidth * props.max + left,
top: itemRectInfo.top,
bottom: itemRectInfo.bottom,
})
let initValue = props.modelValue || 0
// 初始化完成后,选中值为min
if (props.modelValue && props.modelValue < props.min) {
initValue = props.min
}
activeItemWidth.value = initValue * componentItemWidth
updateValue(initValue)
} catch (err) {
if (initCount > 10) {
initCount = 0
debugWarn('TnRate', `获取组件容器信息失败: ${err}`)
return
}
initCount++
setTimeout(() => {
getComponentRectInfo()
}, 300)
}
}
// 组件滑动事件
const onTouchStart = (event: TouchEvent) => {
rateTouchStartHandler(event)
}
const onTouchMove = (event: TouchEvent) => {
rateTouchMoveHandler(event)
// // #ifdef APP-PLUS
// return
// // #endif
if (props.readonly) return
activeItemWidth.value = rateCurrentX.value
}
const onTouchEnd = (event: TouchEvent) => {
rateTouchEndHandler(event)
if (props.readonly) return
if (props.allowHalf) {
// 滑动结束后,判断当前滑动的距离是否为item的宽度的一半
const componentItemWidthHalf = componentItemWidth / 2
let count = Math.ceil(rateCurrentX.value / componentItemWidthHalf)
if (count % 2 !== 0) {
if (
rateCurrentX.value <
componentItemWidthHalf * (count - 1) + componentItemWidthHalf / 2
) {
count -= 1
}
} else {
if (
rateCurrentX.value <
componentItemWidthHalf * (count - 1) + componentItemWidthHalf / 3
) {
count -= 1
}
}
if (
rateCurrentX.value <
componentItemWidthHalf * (count - 1) + componentItemWidthHalf / 2 &&
count % 2 !== 0
) {
count -= 1
}
// 判断是否小于最小值
if (count < props.min * 2) {
count = props.min * 2
}
activeItemWidth.value = componentItemWidthHalf * count
updateValue(count / 2)
} else {
let count = Math.ceil(rateCurrentX.value / componentItemWidth)
if (count > 1 && rateCurrentX.value < componentItemWidth * (count - 1)) {
count -= 1
}
// 判断是否小于最小值
if (count < props.min) {
count = props.min
}
activeItemWidth.value = componentItemWidth * count
updateValue(count)
}
}
onMounted(() => {
// #ifndef APP-PLUS || MP-ALIPAY
nextTick(() => {
getComponentRectInfo()
})
// #endif
// #ifdef APP-PLUS || MP-ALIPAY
setTimeout(() => {
getComponentRectInfo()
}, 500)
// #endif
})
return {
componentId,
rateItemData,
activeItemWidth,
onTouchStart,
onTouchMove,
onTouchEnd,
}
}
@@ -0,0 +1,3 @@
import type Rate from './rate.vue'
export type TnRateInstance = InstanceType<typeof Rate>
+108
View File
@@ -0,0 +1,108 @@
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../constants'
import { buildProps, definePropType, isNumber } from '../../../utils'
import type { ExtractPropTypes } from 'vue'
export type RateCustomData = {
/**
* @description 未选中时的图标
*/
inactiveIcon?: string
/**
* @description 选中时的图标
*/
activeIcon?: string
/**
* @description 未选中时的颜色
*/
inactiveColor?: string
/**
* @description 选中时的颜色
*/
activeColor?: string
}
export type RateCustomDataMap = Record<number, RateCustomData>
export const rateProps = buildProps({
/**
* @description 选中的数量
*/
modelValue: {
type: Number,
default: 0,
},
/**
* @description 最小值
*/
min: {
type: Number,
default: 0,
},
/**
* @description 最大值
*/
max: {
type: Number,
default: 5,
},
/**
* @description 是否允许半选
*/
allowHalf: Boolean,
/**
* @description 是否只读
*/
readonly: Boolean,
/**
* @description 尺寸大小,可选值为 `sm`、`lg`、`xl` 或者设置指定的尺寸
*/
size: String,
/**
* @description 未选中时的图标
*/
inactiveIcon: {
type: String,
default: 'star',
},
/**
* @description 选中时的图标
*/
activeIcon: {
type: String,
default: 'star-fill',
},
/**
* @description 未选中时的颜色,以tn开头使用图鸟内置的颜色
*/
inactiveColor: String,
/**
* @description 选中时的颜色,以tn开头使用图鸟内置的颜色
*/
activeColor: String,
/**
* @description 每个图标的间距,默认单位 `rpx`
*/
gutter: String,
/**
* @description 自定义图标信息
*/
customData: {
type: definePropType<RateCustomDataMap>(Object),
default: () => ({}),
},
/**
* @description 值发生修改时是否触发表单验证
*/
validateEvent: {
type: Boolean,
default: true,
},
})
export const rateEmits = {
[UPDATE_MODEL_EVENT]: (value: number) => isNumber(value),
[CHANGE_EVENT]: (value: number) => isNumber(value),
}
export type RateProps = ExtractPropTypes<typeof rateProps>
export type RateEmits = typeof rateEmits
+60
View File
@@ -0,0 +1,60 @@
<script lang="ts" setup>
import TnIcon from '../../icon/src/icon.vue'
import { rateEmits, rateProps } from './rate'
import { useRate, useRateCustomStyle } from './composables'
const props = defineProps(rateProps)
const emits = defineEmits(rateEmits)
const {
componentId,
rateItemData,
activeItemWidth,
onTouchStart,
onTouchMove,
onTouchEnd,
} = useRate(props, emits)
const { ns, rateClass, itemClass, itemStyle } = useRateCustomStyle(props)
</script>
<template>
<view
:id="componentId"
:class="[rateClass]"
@touchstart.stop.prevent="onTouchStart"
@touchmove.stop.prevent="onTouchMove"
@touchend.stop.prevent="onTouchEnd"
>
<!-- 默认显示的图标 -->
<view :class="[ns.e('container'), ns.e('inactive-container')]">
<view
v-for="(item, index) in rateItemData"
:key="index"
:class="[itemClass('inactive', item.inactive)]"
:style="itemStyle('inactive', item.inactive)"
>
<TnIcon :name="item.inactive.icon" />
</view>
</view>
<!-- 选中显示的图标 -->
<view
:class="[ns.e('container'), ns.e('active-container')]"
:style="{
width: `${activeItemWidth}px`,
}"
>
<view
v-for="(item, index) in rateItemData"
:key="index"
:class="[itemClass('active', item.active)]"
:style="itemStyle('active', item.active)"
>
<TnIcon :name="item.active.icon" />
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/rate.scss';
</style>
+12
View File
@@ -0,0 +1,12 @@
export interface RateItemData {
active: RateItem
inactive: RateItem
}
export interface RateItem {
icon: string
color: {
class: string
style: string
}
}