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,3 @@
export * from './use-swipe-action'
export * from './swipe-action-options'
export * from './use-swipe-action-item'
@@ -0,0 +1,91 @@
import { computed, ref } from 'vue'
import { useComponentColor } from '../../../../hooks'
import { isEmptyVariableInDefault } from '../../../../utils'
import type { CSSProperties } from 'vue'
import type { SwipeActionItemProps } from '../swipe-action-item'
interface ColorInfo {
class: string
style: string
}
interface OptionsData {
icon: string
text: string
textColor: ColorInfo
bgColor: ColorInfo
round: boolean
disabled: boolean
}
type OptionClassValue = (index: number) => string
type OptionStyleValue = (index: number) => CSSProperties
export const useSwipeActionOptions = (props: SwipeActionItemProps) => {
// 菜单配置项
const options = computed<OptionsData[]>(() => {
return props.options.map((item) => {
const textColor = ref(item.textColor)
const bgColor = ref(item.bgColor)
const [textColorClass, textColoeStyle] = useComponentColor(
textColor,
'text'
)
const [bgColorClass, bgColorStyle] = useComponentColor(bgColor, 'bg')
return {
icon: isEmptyVariableInDefault(item.icon, ''),
text: isEmptyVariableInDefault(item.text, ''),
textColor: {
class: textColorClass.value,
style: textColoeStyle.value,
},
bgColor: {
class: bgColorClass.value,
style: bgColorStyle.value,
},
round: isEmptyVariableInDefault(item.round, false),
disabled: isEmptyVariableInDefault(item.disabled, false),
}
})
})
// 菜单配置项颜色对应的类
const optionColorClass = computed<OptionClassValue>(() => {
return (index: number) => {
const cls: string[] = []
const option = options.value[index]
if (option) {
if (option.textColor.class) cls.push(option.textColor.class)
if (option.bgColor.class) cls.push(option.bgColor.class)
}
return cls.join(' ')
}
})
// 菜单配置项颜色对应样式
const optionColorStyle = computed<OptionStyleValue>(() => {
return (index: number) => {
const style: CSSProperties = {}
const option = options.value[index]
if (option) {
if (!option.textColor.class)
style.color = option.textColor.style || 'var(--tn-color-white)'
if (!option.bgColor.class)
style.backgroundColor =
option.bgColor.style || 'var(--tn-color-primary)'
}
return style
}
})
return {
options,
optionColorClass,
optionColorStyle,
}
}
@@ -0,0 +1,245 @@
import {
computed,
getCurrentInstance,
inject,
nextTick,
onMounted,
onUnmounted,
ref,
watch,
} from 'vue'
import { swipeActionContextKey } from '../../../../tokens'
import { useSelectorQuery, useTouch } from '../../../../hooks'
import {
debugWarn,
generateId,
isEmptyDoubleVariableInDefault,
} from '../../../../utils'
import type { SwipeActionItemProps } from '../swipe-action-item'
export const useSwipeActionItem = (props: SwipeActionItemProps) => {
const instance = getCurrentInstance()
if (!instance) {
debugWarn('TnSwipeActionItem', '请在 setup 中使用 useSwipeActionItem')
}
const { uid } = instance!
const swipeActionContext = inject(swipeActionContextKey, null)
if (!swipeActionContext) {
debugWarn(
'TnSwipeActionItem',
'请在 TnSwipeAction 中使用 TnSwipeActionItem'
)
}
const componentId = `tsai-${generateId()}`
const optionComponentClass = `${componentId}-option`
const { getSelectorNodeInfo, getSelectorNodeInfos } =
useSelectorQuery(instance)
const {
deltaX: swipeActionItemDeltaX,
distanceX: swipeActionItemDistanceX,
distanceY: swipeActionItemDistanceY,
isVertical: swipeActionItemTouchIsVertical,
updateOptions: updateSwipeActionItemTouchOptions,
onTouchStart: swipeActionItemTouchStartEvent,
onTouchMove: swipeActionItemTouchMoveEvent,
onTouchEnd: swipeActionItemTouchEndEvent,
} = useTouch()
// 当前item的菜单是否为打开状态
const isOptionsOpen = computed<boolean>(() =>
swipeActionContext!.activeUid.includes(uid)
)
// 点击options后是否自动关闭
const autoClose = computed<boolean>(() =>
isEmptyDoubleVariableInDefault(
props?.autoClose,
swipeActionContext?.autoClose,
true
)
)
// 禁止滑动
const disabledSwipe = computed<boolean>(
() => props.disabled || !props.options?.length
)
watch(
() => disabledSwipe.value,
(val) => {
updateSwipeActionItemTouchOptions({
disabled: val,
})
},
{
immediate: true,
}
)
// 操作菜单的长度
const optionsMenuWidth = ref(0)
// 获取当前组件节点信息
let initCount = 0
const getComponentNodeInfo = async () => {
try {
const componentRectInfo = await getSelectorNodeInfo(`#${componentId}`)
let optionsRectInfo: UniApp.NodeInfo[] = []
if (props.options?.length) {
optionsRectInfo = await getSelectorNodeInfos(`.${optionComponentClass}`)
}
initCount = 0
updateSwipeActionItemTouchOptions({
left: componentRectInfo.left,
right: componentRectInfo.right,
top: componentRectInfo.top,
bottom: componentRectInfo.bottom,
})
optionsMenuWidth.value = optionsRectInfo.reduce((prev, curr) => {
return prev + (curr.width || 0)
}, 0)
} catch (err) {
if (initCount > 10) {
initCount = 0
debugWarn('TnSwipeActionItem', `获取节点信息失败: ${err}`)
return
}
initCount++
setTimeout(() => {
getComponentNodeInfo()
}, 150)
}
}
// 内容距离左边移动的距离
const contentRightDistance = ref<number>(0)
// 标记是否正在滑动,正在手动滑动的时候暂停动画
const isSwipe = ref<boolean>(false)
// 内容滑动事件
const onTouchStart = (event: any) => {
swipeActionItemTouchStartEvent(event)
}
const onTouchMove = (event: any) => {
event.preventDefault()
event.stopPropagation()
swipeActionItemTouchMoveEvent(event)
isSwipe.value = true
// 如果垂直方向上的移动距离过大则不允许进行滑动
if (
swipeActionItemDistanceX.value < 3 &&
swipeActionItemDistanceY.value > 5
) {
return
}
// 如果当前菜单已经打开了则根据滑动的距离计算内容滑动的距离
if (isOptionsOpen.value) {
contentRightDistance.value =
optionsMenuWidth.value - swipeActionItemDeltaX.value
} else {
contentRightDistance.value = -swipeActionItemDeltaX.value
}
if (contentRightDistance.value < 0) contentRightDistance.value = 0
if (contentRightDistance.value > optionsMenuWidth.value)
contentRightDistance.value = optionsMenuWidth.value
}
const onTouchEnd = (event: any) => {
swipeActionItemTouchEndEvent(event)
// 如果垂直方向上的移动距离过大则不允许进行滑动
if (
swipeActionItemDistanceX.value < 3 &&
swipeActionItemDistanceY.value > 5
) {
return
}
isSwipe.value = false
// 如果滑动的距离大于展开阈值则显示操作菜单
if (-swipeActionItemDeltaX.value > props.threshold) {
openOptionsMenu()
} else {
closeOptionsMenu()
updateAllItemOptionStatus()
}
if (swipeActionItemTouchIsVertical.value) {
updateAllItemOptionStatus()
}
}
// 关闭菜单
const closeOptionsMenu = () => {
isSwipe.value = false
contentRightDistance.value = 0
}
// 更新所有item的菜单状态
const updateAllItemOptionStatus = () => {
if (isOptionsOpen.value) {
swipeActionContext?.setActiveItem(uid)
} else {
if (swipeActionContext?.exclusive) {
swipeActionContext?.closeAllItemOption()
}
}
}
// 打开菜单
const openOptionsMenu = () => {
isSwipe.value = false
contentRightDistance.value = optionsMenuWidth.value
swipeActionContext?.setActiveItem(uid)
}
watch(
() => isOptionsOpen.value,
(val) => {
if (!val) {
closeOptionsMenu()
}
}
)
// 监听菜单操作点击事件
const optionClickEvent = (index: number) => {
const option = props.options![index]
if (option.disabled) return
swipeActionContext?.optionClick(uid, index)
if (autoClose.value) {
closeOptionsMenu()
}
}
onMounted(() => {
swipeActionContext?.addItem({
uid,
})
nextTick(() => {
getComponentNodeInfo()
})
})
onUnmounted(() => {
swipeActionContext?.removeItem(uid)
})
return {
componentId,
optionComponentClass,
isOptionsOpen,
optionsMenuWidth,
contentRightDistance,
isSwipe,
onTouchStart,
onTouchMove,
onTouchEnd,
optionClickEvent,
}
}
@@ -0,0 +1,65 @@
import { provide, reactive, ref, toRefs } from 'vue'
import { swipeActionContextKey } from '../../../../tokens'
import { useOrderedChildren } from '../../../../hooks'
import type { SetupContext } from 'vue'
import type { SwipeActionItemContext } from '../../../../tokens'
import type { SwipeActionEmits, SwipeActionProps } from '../swipe-action'
export const useSwipeAction = (
props: SwipeActionProps,
emits: SetupContext<SwipeActionEmits>['emit']
) => {
const {
children: items,
addChild: addItem,
removeChild: removeItem,
} = useOrderedChildren<SwipeActionItemContext>()
// 当前激活的UID
const activeUid = ref<number[]>([])
// 设置当前激活的item
const setActiveItem = (uid: number) => {
const itemIndex = items.value.findIndex((item) => item.uid === uid)
const index = activeUid.value.indexOf(uid)
if (props.exclusive) {
activeUid.value = index !== -1 ? [] : [uid]
} else {
if (index !== -1) {
activeUid.value.splice(index, 1)
} else {
activeUid.value.push(uid)
}
}
emits('open', itemIndex)
}
// 关闭所有item的菜单
const closeAllItemOption = () => {
activeUid.value = []
}
// 回调当前点击的选项
const optionClick = (uid: number, optionIndex: number) => {
const itemIndex = items.value.findIndex((item) => item.uid === uid)
emits('select', itemIndex, optionIndex)
}
provide(
swipeActionContextKey,
reactive({
...toRefs(props),
items,
addItem,
removeItem,
activeUid,
setActiveItem,
closeAllItemOption,
optionClick,
})
)
}
@@ -0,0 +1,5 @@
import type SwipeAction from './swipe-action.vue'
import type SwipeActionItem from './swipe-action-item.vue'
export type TnSwipeActionInstance = InstanceType<typeof SwipeAction>
export type TnSwipeActionItemInstance = InstanceType<typeof SwipeActionItem>
@@ -0,0 +1,56 @@
import { buildProps, definePropType } from '../../../utils'
import { swipeActionBaseProps } from '../../base/common-props/swipe-action'
import type { ExtractPropTypes } from 'vue'
export type SwipeActionItemOptionItem = {
/**
* @description 菜单文字
*/
text?: string
/**
* @description 菜单图标
*/
icon?: string
/**
* @description 菜单字体颜色,以tn开头使用图鸟内置的颜色
*/
textColor?: string
/**
* @description 菜单背景颜色,以tn开头使用图鸟内置的颜色
*/
bgColor?: string
/**
* @description 菜单禁止点击
*/
disabled?: boolean
/**
* @description 圆形菜单
*/
round?: boolean
}
export type SwipeActionItemOption = SwipeActionItemOptionItem[]
export const swipeActionItemProps = buildProps({
...swipeActionBaseProps,
/**
* @description 菜单配置项
*/
options: {
type: definePropType<SwipeActionItemOption>(Array),
default: () => [],
},
/**
* @description 是否禁用
*/
disabled: Boolean,
/**
* @description 滑动阈值,单位 px,大于该值则滑动动作生效
*/
threshold: {
type: Number,
default: 20,
},
})
export type SwipeActionItemProps = ExtractPropTypes<typeof swipeActionItemProps>
@@ -0,0 +1,88 @@
<script lang="ts" setup>
import TnIcon from '../../icon/src/icon.vue'
import { useNamespace } from '../../../hooks'
import { swipeActionItemProps } from './swipe-action-item'
import { useSwipeActionItem, useSwipeActionOptions } from './composables'
const props = defineProps(swipeActionItemProps)
const ns = useNamespace('swipe-action-item')
const {
componentId,
optionComponentClass,
optionsMenuWidth,
contentRightDistance,
isSwipe,
onTouchStart,
onTouchMove,
onTouchEnd,
optionClickEvent,
} = useSwipeActionItem(props)
const { options, optionColorClass, optionColorStyle } =
useSwipeActionOptions(props)
</script>
// #ifdef MP-WEIXIN
<script lang="ts">
export default {
options: {
// 在微信小程序中将组件节点渲染为虚拟节点,更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
virtualHost: true,
},
}
</script>
// #endif
<template>
<view :id="componentId" :class="[ns.b()]">
<!-- 操作菜单 -->
<view :class="[ns.e('options')]">
<view
v-for="(item, index) in options"
:key="index"
:class="[
optionComponentClass,
ns.e('option-item'),
ns.is('disabled', item.disabled),
ns.is('round', item.round),
]"
:style="{
right: `-${optionsMenuWidth - contentRightDistance}px`,
transitionDuration: `${isSwipe ? 0 : 300}ms`,
}"
@tap.stop="optionClickEvent(index)"
>
<view
:class="[ns.e('option-item-content'), optionColorClass(index)]"
:style="optionColorStyle(index)"
>
<view v-if="item.icon" class="icon">
<TnIcon :name="item.icon" />
</view>
<view v-if="item.text && !item.round" class="text">
{{ item.text }}
</view>
</view>
</view>
</view>
<!-- 内容 -->
<view
:class="[ns.e('content')]"
:style="{
right: `${contentRightDistance}px`,
transitionDuration: `${isSwipe ? 0 : 300}ms`,
}"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<slot />
</view>
</view>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/swipe-action-item.scss';
</style>
@@ -0,0 +1,30 @@
import { buildProps, isNumber } from '../../../utils'
import { swipeActionBaseProps } from '../../base/common-props/swipe-action'
import type { ExtractPropTypes } from 'vue'
export const swipeActionProps = buildProps({
...swipeActionBaseProps,
/**
* @description 只允许一个item处于打开状态
*/
exclusive: {
type: Boolean,
default: true,
},
})
export const swipeActionEmits = {
/**
* @description 打开时触发
*/
open: (index: number) => isNumber(index),
/**
* @description 点击选项时触发
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
select: (index: number, optionIndex: number) => true,
}
export type SwipeActionProps = ExtractPropTypes<typeof swipeActionProps>
export type SwipeActionEmits = typeof swipeActionEmits
@@ -0,0 +1,22 @@
<script lang="ts" setup>
import { useNamespace } from '../../../hooks'
import { swipeActionEmits, swipeActionProps } from './swipe-action'
import { useSwipeAction } from './composables'
const props = defineProps(swipeActionProps)
const emits = defineEmits(swipeActionEmits)
const ns = useNamespace('swipe-action')
useSwipeAction(props, emits)
</script>
<template>
<view :class="[ns.b()]">
<slot />
</view>
</template>
<style lang="scss" scoped>
@import './../../../theme-chalk/src/swipe-action.scss';
</style>